Jak określić, że typ zwracanej metody jest taki sam jak sama klasa?

410

Mam następujący kod w python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

Ale mój redaktor (PyCharm) mówi, że pozycji referencyjnej nie można rozwiązać (w __add__metodzie). Jak mam określić, że spodziewam się, że typ zwrotu będzie typu Position?

Edycja: Myślę, że to właściwie problem PyCharm. W rzeczywistości wykorzystuje informacje zawarte w ostrzeżeniach i uzupełnianiu kodu

Ale popraw mnie, jeśli się mylę, i potrzebuję użyć innej składni.

Michael van Gerwen
źródło

Odpowiedzi:

574

TL; DR : jeśli używasz Python 4.0, to po prostu działa. Na dzień dzisiejszy (2019) w wersji 3.7+ musisz włączyć tę funkcję, używając przyszłej instrukcji ( from __future__ import annotations) - w Pythonie 3.6 lub niższym użyj ciągu znaków.

Myślę, że masz ten wyjątek:

NameError: name 'Position' is not defined

Wynika to z faktu, że Positionnależy go zdefiniować, zanim będzie można go użyć w adnotacji, chyba że używa się języka Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 wprowadza PEP 563: odroczona ocena adnotacji . Moduł korzystający z przyszłej instrukcji from __future__ import annotationsautomatycznie zapisuje adnotacje jako ciągi znaków:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Jest to zaplanowane jako domyślne w Pythonie 4.0. Ponieważ Python nadal jest językiem o dynamicznym pisaniu, więc sprawdzanie typów nie jest wykonywane w czasie wykonywania, adnotacje podczas pisania nie powinny mieć wpływu na wydajność, prawda? Źle! Przed Pythonie 3.7 modułu wpisując kiedyś jeden z najwolniejszych modułów Python w rdzeniu więc jeśli was import typingwidać aż do 7-krotny wzrost wydajności podczas aktualizacji do 3.7.

Python <3.7: użyj ciągu

Według PEP 484 powinieneś użyć ciągu zamiast samej klasy:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Jeśli używasz frameworka Django, może to być znane, ponieważ modele Django używają również ciągów dla referencji do przodu (definicje kluczy obcych, w których model obcy jest selflub nie został jeszcze zadeklarowany). Powinno to działać z Pycharmem i innymi narzędziami.

Źródła

Odpowiednie części PEP 484 i PEP 563 , aby oszczędzić ci podróży:

Prześlij referencje

Gdy podpowiedź typu zawiera nazwy, które nie zostały jeszcze zdefiniowane, definicję tę można wyrazić dosłownie jako ciąg znaków, co zostanie rozwiązane później.

Sytuacja, w której zdarza się to często, to definicja klasy kontenera, w której definiowana klasa występuje w sygnaturze niektórych metod. Na przykład następujący kod (początek prostej implementacji drzewa binarnego) nie działa:

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

Aby rozwiązać ten problem, piszemy:

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

Literał łańcuchowy powinien zawierać prawidłowe wyrażenie w języku Python (tj. Kompilacja (lit, '', 'eval') powinna być prawidłowym obiektem kodu) i powinna być oceniana bez błędów po pełnym załadowaniu modułu. Lokalna i globalna przestrzeń nazw, w której jest obliczana, powinny być tymi samymi przestrzeniami nazw, w których oceniane byłyby domyślne argumenty tej samej funkcji.

i PEP 563:

W Pythonie 4.0 adnotacje funkcji i zmiennych nie będą już oceniane w czasie definiowania. Zamiast tego w odpowiednim __annotations__słowniku zachowana zostanie forma łańcucha . Mechanizmy sprawdzania typu statycznego nie zauważą żadnej różnicy w zachowaniu, podczas gdy narzędzia używające adnotacji w czasie wykonywania będą musiały wykonać odroczoną ocenę.

...

Funkcję opisaną powyżej można włączyć począwszy od Pythona 3.7, korzystając z następującego specjalnego importu:

from __future__ import annotations

Rzeczy, które możesz pokusić się zamiast tego

A. Zdefiniuj manekina Position

Przed definicją klasy umieść fikcyjną definicję:

class Position(object):
    pass


class Position(object):
    ...

Spowoduje to usunięcie NameErrori może nawet wyglądać OK:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

Ale czy to jest?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

B. Łatka-małpa w celu dodania adnotacji:

Możesz wypróbować magię programowania w języku Python i napisać dekorator, aby załatać definicję klasy w celu dodania adnotacji:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

Dekorator powinien być odpowiedzialny za równowartość tego:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

Przynajmniej wydaje się słuszne:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Prawdopodobnie za dużo kłopotów.

Wniosek

Jeśli używasz wersji 3.6 lub niższej, użyj literału łańcuchowego zawierającego nazwę klasy, w wersji 3.7 użyj from __future__ import annotationsi to po prostu zadziała.

Paulo Scardine
źródło
2
Racja, jest to mniej problem PyCharm, a bardziej problem Python 3.5 PEP 484. Podejrzewam, że dostaniesz to samo ostrzeżenie, jeśli uruchomisz je za pomocą narzędzia typu mypy.
Paul Everitt,
23
> jeśli używasz Python 4.0, to po prostu działa, czy widziałeś Sarah Connor? :)
scrutari
@ JoelBerkeley Właśnie go przetestowałem i parametry typu działały dla mnie w wersji 3.6, po prostu nie zapomnij zaimportować, typingponieważ każdy typ, którego używasz, musi być objęty zakresem podczas oceny łańcucha.
Paulo Scardine,
Ach, mój błąd, byłem tylko oddanie ''okrągły klasę, a nie parametry typu
joelb
5
Ważna uwaga dla każdego, kto korzysta from __future__ import annotations- należy ją zaimportować przed wszystkimi innymi importami.
Artur,
16

Określenie typu jako łańcucha jest w porządku, ale zawsze docenia mnie to, że w zasadzie omijamy parser. Więc lepiej nie pisuj źle żadnego z tych literalnych ciągów:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Niewielką odmianą jest użycie związanego typu maszynowego, przynajmniej wtedy, gdy deklarujesz typograficzny, musisz napisać ciąg tylko raz:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)
vbraun
źródło
8
Chciałbym, żeby Python musiał typing.Selfto wyraźnie określić.
Alexander Huszagh
2
Przybyłem tutaj, żeby sprawdzić, czy typing.Selfistnieje coś takiego jak twój . Zwrócenie łańcucha zakodowanego na stałe nie zwraca poprawnego typu przy wykorzystaniu polimorfizmu. W moim przypadku chciałem wdrożyć metodę klasy deserializacji . Postanowiłem zwrócić dyktando (kwargs) i zadzwonić some_class(**some_class.deserialize(raw_data)).
Scott P.
Zastosowane tutaj adnotacje typu są odpowiednie przy prawidłowej implementacji w celu użycia podklas. Jednak implementacja zwraca Position, a nie klasę, więc powyższy przykład jest technicznie niepoprawny. Implementacja powinna zostać zastąpiona Position(czymś takim self.__class__(.
Sam Bull
Dodatkowo adnotacje mówią, że typ zwrotu zależy od other, ale najprawdopodobniej zależy od tego self. Trzeba więc umieścić adnotację, selfaby opisać prawidłowe zachowanie (a może otherpo prostu Positionpokazać, że nie jest związana z typem zwrotu). Można tego również użyć w przypadkach, gdy pracujesz tylko self. np.def __aenter__(self: T) -> T:
Sam Bull
15

Nazwa „Pozycja” nie jest dostępna w czasie analizy samego ciała klasy. Nie wiem, w jaki sposób używasz deklaracji typu, ale PEP 484 Pythona - tego powinien używać większość trybów, jeśli korzystając z tych wskazówek dotyczących pisania, możesz po prostu umieścić nazwę jako ciąg znaków w tym miejscu:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Sprawdź https://www.python.org/dev/peps/pep-0484/#forward-references - narzędzia zgodne z tym będą wiedziały, jak rozpakować stamtąd nazwę klasy i z niej skorzystać (zawsze ważne jest, aby mieć pamiętając, że sam język Python nie robi nic z tych adnotacji - są one zwykle przeznaczone do analizy kodu statycznego lub można mieć bibliotekę / strukturę do sprawdzania typu w czasie wykonywania - ale trzeba to wyraźnie ustawić).

aktualizacja Ponadto, począwszy od Python 3.8, sprawdź pep-563 - od Python 3.8 można pisać, from __future__ import annotationsaby odroczyć ocenę adnotacji - klasy referencyjne powinny działać od razu.

jsbueno
źródło
9

Gdy podpowiedź typu tekstowego jest akceptowalna, __qualname__można również użyć elementu. Zawiera nazwę klasy i jest dostępna w treści definicji klasy.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

W ten sposób zmiana nazwy klasy nie oznacza modyfikacji podpowiedzi typu. Ale osobiście nie spodziewałbym się, że inteligentne edytory kodu dobrze poradzą sobie z tym formularzem.

Yvon DUTAPIS
źródło
1
Jest to szczególnie przydatne, ponieważ nie koduje nazwy klasy, więc działa w podklasach.
Florian Brucker,
Nie jestem pewien, czy to zadziała z odroczoną oceną adnotacji (PEP 563), więc zadałem o to pytanie .
Florian Brucker,