Wie gebe ich an, dass der Rückgabetyp einer Methode mit der Klasse selbst identisch ist?

410

Ich habe den folgenden Code in 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)

Mein Editor (PyCharm) sagt jedoch, dass die Referenzposition (in der __add__Methode) nicht aufgelöst werden kann . Wie soll ich angeben, dass ich erwarte, dass der Rückgabetyp vom Typ ist Position?

Bearbeiten: Ich denke, das ist eigentlich ein PyCharm-Problem. Es verwendet die Informationen tatsächlich in seinen Warnungen und der Code-Vervollständigung

Aber korrigieren Sie mich, wenn ich falsch liege und eine andere Syntax verwenden muss.

Michael van Gerwen
quelle

Antworten:

574

TL; DR : Wenn Sie Python 4.0 verwenden, funktioniert es einfach. Ab heute (2019) in Version 3.7+ müssen Sie diese Funktion mithilfe einer zukünftigen Anweisung ( from __future__ import annotations) aktivieren. Verwenden Sie für Python 3.6 oder niedriger eine Zeichenfolge.

Ich denke du hast diese Ausnahme:

NameError: name 'Position' is not defined

Dies liegt daran, Positiondass definiert werden muss, bevor Sie es in einer Anmerkung verwenden können, es sei denn, Sie verwenden Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 wird eingeführt PEP 563 ein: Verschiebung der Auswertung von Anmerkungen . Ein Modul, das die zukünftige Anweisung verwendet from __future__ import annotations, speichert Anmerkungen automatisch als Zeichenfolgen:

from __future__ import annotations

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

Dies soll in Python 4.0 zum Standard werden. Da Python immer noch eine dynamisch typisierte Sprache ist und zur Laufzeit keine Typprüfung durchgeführt wird, sollten Tippanmerkungen keine Auswirkungen auf die Leistung haben, oder? Falsch! Vor Python 3.7 war das Typisierungsmodul eines der langsamsten Python-Module im Kern so , wenn Sie import typingSie werden sehen , bis zu 7 - mal in der Leistung zu erhöhen , wenn Sie auf 3,7 aktualisieren.

Python <3.7: Verwenden Sie eine Zeichenfolge

Nach PEP 484 sollten Sie anstelle der Klasse selbst eine Zeichenfolge verwenden:

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

Wenn Sie das Django-Framework verwenden, ist dies möglicherweise bekannt, da Django-Modelle auch Zeichenfolgen für Vorwärtsreferenzen verwenden (Fremdschlüsseldefinitionen, bei denen das Fremdmodell selfnoch deklariert ist oder nicht). Dies sollte mit Pycharm und anderen Tools funktionieren.

Quellen

Die relevanten Teile von PEP 484 und PEP 563 , um Ihnen die Reise zu ersparen:

Referenzen weiterleiten

Wenn ein Typhinweis Namen enthält, die noch nicht definiert wurden, kann diese Definition als Zeichenfolgenliteral ausgedrückt werden, um später aufgelöst zu werden.

Eine Situation, in der dies häufig vorkommt, ist die Definition einer Containerklasse, in der die zu definierende Klasse in der Signatur einiger Methoden vorkommt. Der folgende Code (der Beginn einer einfachen Implementierung eines Binärbaums) funktioniert beispielsweise nicht:

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

Um dies zu beheben, schreiben wir:

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

Das String-Literal sollte einen gültigen Python-Ausdruck enthalten (dh compile (lit, '', 'eval') sollte ein gültiges Codeobjekt sein) und nach dem vollständigen Laden des Moduls fehlerfrei ausgewertet werden. Der lokale und globale Namespace, in dem er ausgewertet wird, sollten dieselben Namespaces sein, in denen Standardargumente für dieselbe Funktion ausgewertet werden.

und PEP 563:

In Python 4.0 werden Funktions- und Variablenanmerkungen zur Definitionszeit nicht mehr ausgewertet. Stattdessen wird eine Zeichenfolgenform im jeweiligen __annotations__Wörterbuch beibehalten . Statische Typprüfungen sehen keinen Unterschied im Verhalten, während Tools, die zur Laufzeit Anmerkungen verwenden, eine verschobene Auswertung durchführen müssen.

...

Die oben beschriebene Funktionalität kann ab Python 3.7 mit dem folgenden speziellen Import aktiviert werden:

from __future__ import annotations

Dinge, zu denen Sie möglicherweise versucht sind

A. Definieren Sie einen Dummy Position

Platzieren Sie vor der Klassendefinition eine Dummy-Definition:

class Position(object):
    pass


class Position(object):
    ...

Dies wird das loswerden NameErrorund kann sogar OK aussehen:

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

Aber ist es?

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

B. Affen-Patch, um die Anmerkungen hinzuzufügen:

Vielleicht möchten Sie etwas Python-Meta-Programmiermagie ausprobieren und einen Dekorator schreiben, um die Klassendefinition mit Affen zu patchen, um Anmerkungen hinzuzufügen:

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

Der Dekorateur sollte für das Äquivalent verantwortlich sein:

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

Zumindest scheint es richtig:

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

Wahrscheinlich zu viel Ärger.

Fazit

Wenn Sie 3.6 oder niedriger verwenden, verwenden Sie ein Zeichenfolgenliteral, das den Klassennamen enthält. Verwenden Sie in 3.7 from __future__ import annotationsund es funktioniert einfach.

Paulo Scardine
quelle
2
Richtig, dies ist weniger ein PyCharm-Problem als vielmehr ein Python 3.5 PEP 484-Problem. Ich vermute, Sie würden die gleiche Warnung erhalten, wenn Sie sie über das mypy-Tool ausführen würden.
Paul Everitt
23
> Wenn Sie Python 4.0 verwenden, funktioniert es übrigens nur. Haben Sie Sarah Connor gesehen? :)
scrutari
@JoelBerkeley Ich habe es gerade getestet und Typparameter haben für mich unter 3.6 funktioniert. Vergessen Sie nicht, von zu importieren, typingda jeder Typ, den Sie verwenden, im Gültigkeitsbereich sein muss, wenn die Zeichenfolge ausgewertet wird.
Paulo Scardine
Ah, mein Fehler, ich habe nur ''die Klasse umrundet, nicht die Typparameter
Joelb
5
Wichtiger Hinweis für alle Benutzer from __future__ import annotations- dieser muss vor allen anderen Importen importiert werden.
Artur
16

Das Angeben des Typs als Zeichenfolge ist in Ordnung, aber es regt mich immer ein bisschen auf, dass wir den Parser im Grunde umgehen. Sie sollten also keine dieser wörtlichen Zeichenfolgen falsch schreiben:

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

Eine geringfügige Abweichung besteht darin, einen gebundenen Typevar zu verwenden. Zumindest müssen Sie den String dann nur einmal schreiben, wenn Sie den Typevar deklarieren:

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
quelle
8
Ich wünschte, Python hätte typing.Selfdies explizit spezifizieren müssen.
Alexander Huszagh
2
Ich bin hierher gekommen, um zu sehen, ob so etwas wie dein typing.Selfexistiert. Wenn Sie eine fest codierte Zeichenfolge zurückgeben, wird bei Verwendung des Polymorphismus nicht der richtige Typ zurückgegeben. In meinem Fall wollte ich eine Deserialisierungsklassenmethode implementieren. Ich entschied mich dafür, ein Diktat (kwargs) zurückzugeben und anzurufen some_class(**some_class.deserialize(raw_data)).
Scott P.
Die hier verwendeten Typanmerkungen sind geeignet, wenn dies korrekt implementiert wird, um Unterklassen zu verwenden. Die Implementierung Positionwird jedoch zurückgegeben und nicht die Klasse. Daher ist das obige Beispiel technisch nicht korrekt. Die Implementierung sollte durch so Position(etwas ersetzt werden self.__class__(.
Sam Bull
Zusätzlich sagen die Anmerkungen, dass der Rückgabetyp von abhängt other, aber höchstwahrscheinlich tatsächlich von self. Sie müssten also die Annotation aktivieren self, um das korrekte Verhalten zu beschreiben (und othersollten möglicherweise nur Positionzeigen, dass es nicht an den Rückgabetyp gebunden ist). Dies kann auch für Fälle verwendet werden, in denen Sie nur mit arbeiten self. zBdef __aenter__(self: T) -> T:
Sam Bull
15

Der Name 'Position' ist zum Zeitpunkt der Analyse des Klassenkörpers selbst nicht verfügbar. Ich weiß nicht, wie Sie die Typdeklarationen verwenden, aber Pythons PEP 484 - was die meisten Modi verwenden sollten, wenn Sie diese Tipphinweise verwenden, besagt, dass Sie den Namen an dieser Stelle einfach als Zeichenfolge einfügen können:

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

Überprüfen Sie https://www.python.org/dev/peps/pep-0484/#forward-references - Tools, die dem entsprechen, können den Klassennamen von dort auspacken und verwenden. (Dies ist immer wichtig Denken Sie daran, dass die Python-Sprache selbst nichts von diesen Anmerkungen macht - sie sind normalerweise für die Analyse von statischem Code gedacht, oder man könnte eine Bibliothek / ein Framework für die Typprüfung zur Laufzeit haben - aber das müssen Sie explizit festlegen).

Update Überprüfen Sie ab Python 3.8 auch pep-563 - ab Python 3.8 ist es möglich zu schreiben from __future__ import annotations, um die Auswertung von Anmerkungen zu verschieben - Vorwärtsreferenzierungsklassen sollten problemlos funktionieren.

jsbueno
quelle
9

Wenn ein auf Zeichenfolgen basierender Typhinweis akzeptabel ist, kann das __qualname__Element auch verwendet werden. Es enthält den Namen der Klasse und ist im Hauptteil der Klassendefinition verfügbar.

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

Auf diese Weise bedeutet das Umbenennen der Klasse nicht, dass die Typhinweise geändert werden. Ich persönlich würde jedoch nicht erwarten, dass Smart-Code-Editoren mit diesem Formular gut umgehen.

Yvon DUTAPIS
quelle
1
Dies ist besonders nützlich, da der Klassenname nicht fest codiert wird und daher weiterhin in Unterklassen funktioniert.
Florian Brucker
Ich bin mir nicht sicher, ob dies mit der verschobenen Auswertung von Anmerkungen (PEP 563) funktioniert, daher habe ich eine Frage dazu gestellt .
Florian Brucker