Podpowiedzi typu Python bez cyklicznego importu

123

Próbuję podzielić moją ogromną klasę na dwie; cóż, w zasadzie do klasy "main" i mieszanki z dodatkowymi funkcjami, takimi jak:

main.py plik:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py plik:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

Chociaż działa to dobrze, podpowiedź MyMixin.func2dotycząca typu oczywiście nie może działać. Nie mogę importować main.py, ponieważ otrzymywałbym import cykliczny i bez podpowiedzi mój redaktor (PyCharm) nie może powiedzieć, co selfjest.

Używam Pythona 3.4 i chcę przejść na 3.5, jeśli jest tam dostępne rozwiązanie.

Czy jest jakiś sposób, żebym mógł podzielić moją klasę na dwa pliki i zachować wszystkie „połączenia”, aby moje IDE nadal oferowało mi automatyczne uzupełnianie i wszystkie inne korzyści, które z niego pochodzą, znając typy?

velis
źródło
2
Nie sądzę, aby normalnie trzeba było dodawać adnotacje do typu self, ponieważ zawsze będzie to podklasa bieżącej klasy (a każdy system sprawdzania typu powinien być w stanie samodzielnie to rozgryźć). Czy func2próby wywołania func1, które nie są zdefiniowane w MyMixin? Może powinno być (jako abstractmethod, może)?
Blckknght
zwróć również uwagę, że generalnie bardziej szczegółowe klasy (np. twoja mieszanka) powinny znajdować się na lewo od klas bazowych w definicji klasy, tj. class Main(MyMixin, SomeBaseClass)aby metody z bardziej szczegółowej klasy mogły przesłonić te z klasy bazowej
Anentropic
3
Nie jestem pewien, jak przydatne są te komentarze, ponieważ są one styczne do zadawanego pytania. velis nie prosił o przegląd kodu.
Jacob Lee
Wskazówki dotyczące typów w Pythonie z importowanymi metodami klas zapewniają eleganckie rozwiązanie problemu.
Ben Mares

Odpowiedzi:

179

Obawiam się, że generalnie nie ma niezwykle eleganckiego sposobu obsługi cykli importu. Masz do wyboru przeprojektowanie kodu, aby usunąć cykliczną zależność, lub jeśli nie jest to wykonalne, zrób coś takiego:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    def func2(self, some_param: 'Main'):
        ...

TYPE_CHECKINGStała jest zawsze Falseprzy starcie, więc import nie będą oceniane, ale mypy (i innych narzędzi typu sprawdzanie) oceni zawartość tego bloku.

Musimy również przekształcić Mainadnotację typu w ciąg, skutecznie deklarując go do przodu, ponieważ Mainsymbol nie jest dostępny w czasie wykonywania.

Jeśli używasz Pythona 3.7+, możemy przynajmniej pominąć konieczność podawania wyraźnej adnotacji w postaci ciągu, korzystając z PEP 563 :

# some_file.py

from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    # Hooray, cleaner annotations!
    def func2(self, some_param: Main):
        ...

from __future__ import annotationsImport uczyni wszystkie podpowiedzi typu BE strun i pominąć ich oceny. Może to pomóc uczynić nasz kod nieco bardziej ergonomicznym.

Wszystko to powiedziawszy, używanie mixinów z myPy prawdopodobnie będzie wymagało nieco większej struktury niż obecnie. Mypy zaleca podejście, które jest w zasadzie tym, co decezeopisuje - stworzenie ABC, które dziedziczy zarówno twoja, jak Maini MyMixinklasy. Nie zdziwiłbym się, gdybyś musiał zrobić coś podobnego, aby uszczęśliwić pycharma.

Michael0x2a
źródło
4
Dzięki za to. Mój obecny Python 3.4 nie ma typing, ale PyCharm również był z niego zadowolony if False:.
velis
Jedynym problemem jest to, że nie rozpoznaje MyObject jako modelu Django. Model i dlatego dręczy, że atrybuty instancji są definiowane poza__init__
velis
Oto odpowiednia odpowiedź dla typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor
29

Dla osób borykających się z cyklicznymi importami podczas importowania klas tylko do sprawdzania typu: prawdopodobnie będziesz chciał użyć odniesienia do przodu (PEP 484 - Wskazówki dotyczące typu):

Gdy wskazówka dotycząca typu zawiera nazwy, które nie zostały jeszcze zdefiniowane, ta definicja może zostać wyrażona jako literał ciągu, który zostanie rozwiązany później.

Więc zamiast:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

ty robisz:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right
Tomasz Bartkowiak
źródło
Może to PyCharm. Czy używasz najnowszej wersji? Czy próbowałeś File -> Invalidate Caches?
Tomasz Bartkowiak
Dzięki. Przepraszam, usunąłem swój komentarz. Wspomniał, że to działa, ale PyCharm narzeka. Rozwiązałem przy użyciu hackowania if False sugerowanego przez Velisa . Unieważnienie pamięci podręcznej nie rozwiązało problemu. Prawdopodobnie jest to problem PyCharm.
Jacob Lee
1
@JacobLee Zamiast if False:ciebie możesz też from typing import TYPE_CHECKINGi if TYPE_CHECKING:.
luckydonald
11

Większym problemem jest to, że twoje typy nie są rozsądne na początku. MyMixinprzyjmuje zakodowane na sztywno założenie, że zostanie wmieszany Main, podczas gdy może zostać zmieszany z dowolną liczbą innych klas, w którym to przypadku prawdopodobnie się zepsuje. Jeśli twój mixin jest zakodowany na sztywno, aby był mieszany w jedną konkretną klasę, równie dobrze możesz napisać metody bezpośrednio do tej klasy, zamiast je rozdzielać.

Aby poprawnie zrobić to za pomocą rozsądnego pisania, MyMixinnależy zakodować w interfejsie lub klasie abstrakcyjnej w języku Pythona:

import abc


class MixinDependencyInterface(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass


class MyMixin:
    def func2(self: MixinDependencyInterface, xxx):
        self.foo()  # ← mixin only depends on the interface


class Main(MixinDependencyInterface, MyMixin):
    def foo(self):
        print('bar')
zamrozić
źródło
1
Cóż, nie twierdzę, że moje rozwiązanie jest świetne. Właśnie to próbuję zrobić, aby kod był łatwiejszy w zarządzaniu. Twoja sugestia może minąć, ale w rzeczywistości oznaczałoby to po prostu przeniesienie całej klasy Main do interfejsu w moim konkretnym przypadku.
velis
3

Okazuje się, że moja pierwotna próba również była dość bliska rozwiązania. Oto, czego obecnie używam:

# main.py
import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...


# mymixin.py
if False:
    from main import Main

class MyMixin(object):
    def func2(self: 'Main', xxx):  # <--- note the type hint
        ...

Zwróć uwagę na instrukcję import within if False, która nigdy nie jest importowana (ale IDE i tak wie o tym) i używa Mainklasy jako ciągu znaków, ponieważ nie jest ona znana w czasie wykonywania.

velis
źródło
Spodziewałbym się, że spowoduje to ostrzeżenie o martwym kodzie.
Phil
@Phil: tak, wtedy używałem Pythona 3.4. Teraz jest wpisywanie.TYPE_CHECKING
velis
-4

Myślę, że idealnym sposobem powinno być zaimportowanie wszystkich klas i zależności w pliku (jak __init__.py), a następnie from __init__ import *we wszystkich innych plikach.

W tym przypadku jesteś

  1. unikanie wielu odniesień do tych plików i klas oraz
  2. wystarczy dodać tylko jedną linię w każdym z pozostałych plików i
  3. trzecią byłaby pycharm wiedząca o wszystkich klasach, których możesz użyć.
AmirHossein
źródło
1
oznacza to, że ładujesz wszystko wszędzie, jeśli masz dość ciężką bibliotekę, oznacza to, że dla każdego importu musisz załadować całą bibliotekę. + odniesienie będzie działać bardzo wolno.
Omer Shacham
> oznacza to, że ładujesz wszystko wszędzie. >>>> absolutnie nie, jeśli masz wiele plików " init .py" lub innych, i unikaj import *, a mimo to możesz skorzystać z tego prostego podejścia
Sławomir Lenart