
Ich denke, wir gewöhnen uns langsam an die Tatsache, dass Python Typanmerkungen hat: Sie wurden zwei Releases (3.5) in den Annotationen von Funktionen und Methoden ( PEP 484 ) und in der letzten Version (3.6) an Variablen ( PEP 526 ) zurückgebracht.
Da diese beiden PEPs von MyPy inspiriert wurden, werde ich Ihnen sagen, welche weltlichen Freuden und kognitiven Dissonanzen mich bei der Verwendung dieses statischen Analysators sowie des gesamten Typisierungssystems erwarteten.
Disclamer: Ich stelle nicht die Frage nach der Notwendigkeit oder Schädlichkeit der statischen Typisierung in Python. Ich spreche nur von den Fallstricken, die mir bei der Arbeit in einem statisch typisierten Kontext begegnet sind.
Generika (typing.Generic)
Es ist schön, etwas wie List[int]
, Callable[[int, str], None]
in Anmerkungen zu verwenden.
Es ist sehr schön, wenn der Analysator den folgenden Code hervorhebt:
T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'
Was ist jedoch, wenn wir eine Bibliothek schreiben und der Programmierer, der sie verwendet, keinen statischen Analysator verwendet?
Den Benutzer zwingen, die Klasse mit einem Wert zu initialisieren und dann ihren Typ zu speichern?
T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value)
Irgendwie nicht benutzerfreundlich.
Aber was ist, wenn Sie das wollen?
b = Gen[A](B())
Auf der Suche nach einer Antwort auf diese Frage ging ich ein wenig typing
und tauchte in die Welt der Fabriken ein.

Tatsache ist, dass nach dem Initialisieren der Instanz der generischen Klasse das Attribut __args__
Attribut __args__
wird, bei dem es sich um ein Tupel von Typen handelt. Der Zugriff von __init__
sowie von __new__
ist jedoch nicht möglich. Es ist auch nicht in der Metaklasse __call__
. Der Trick besteht darin, dass zum Zeitpunkt der Initialisierung der Unterklasse von Generic
andere Metaklasse _GenericAlias
, die den endgültigen Typ festlegt, entweder nachdem das Objekt einschließlich aller Methoden seiner Metaklasse initialisiert wurde oder wenn __getithem__
wird. Daher gibt es beim Erstellen eines Objekts keine Möglichkeit, generische Typen abzurufen.
Wir werfen diesen Müll, versprachen eine universellere Lösung.Deshalb habe ich mir einen kleinen Deskriptor geschrieben, der dieses Problem löst:
def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]
Infolgedessen wird es natürlich notwendig sein, für eine universellere Verwendung umzuschreiben, aber das Wesentliche ist klar.
[UPD]: Am Morgen habe ich beschlossen, dasselbe wie im typing
selbst zu versuchen, aber einfacher:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[UPD]: Der typing
Ivan Levinsky sagte, beide Optionen könnten unvorhersehbar brechen.
Wie auch immer, Sie können jeden Weg verwenden. Vielleicht ist __class_getitem__
sogar etwas besser, zumindest ist __class_getitem__
eine dokumentierte Spezialmethode (obwohl dies bei Generika nicht der __class_getitem__
ist).
Funktionen und Aliase
Ja, Generika sind gar nicht so einfach:
Wenn wir beispielsweise irgendwo eine Funktion als Argument akzeptieren, wechselt ihre Annotation automatisch von kovariant zu kontravariant:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
Und im Prinzip habe ich keine Beschwerden über Logik, nur muss dies durch generische Aliase gelöst werden:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
Im Allgemeinen muss der Abschnitt über die Variabilität von Typen sorgfältig und mehrmals gelesen werden.
Abwärtskompatibilität
Das ist nicht so heiß: Ab Version 3.7 ist Generic
eine Unterklasse von ABCMeta
, die sehr praktisch und gut ist. Es ist schlecht, dass dies den Code bricht, wenn es auf 3.6 läuft.
Strukturelle Vererbung (Stuctural Suptyping)
Anfangs war ich sehr glücklich: Die Schnittstellen wurden geliefert! Die Rolle der Schnittstellen wird von der Protocol
aus dem Modul @runtime
, mit der Sie in Kombination mit dem Dekorator @runtime
überprüfen können, ob die Klasse die Schnittstelle ohne direkte Vererbung implementiert. MyPy wird auch auf einer tieferen Ebene hervorgehoben.
Allerdings habe ich in der Laufzeit im Vergleich zur Mehrfachvererbung keinen großen praktischen Nutzen festgestellt.
Es scheint, dass der Dekorateur nur das Vorhandensein einer Methode mit dem erforderlichen Namen überprüft, ohne auch nur die Anzahl der Argumente zu überprüfen, ganz zu schweigen von der Eingabe:
import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)
Auf der anderen Seite verhält sich MyPy wiederum intelligenter und hebt die Inkompatibilität von Typen hervor:
push_func(a, 1) push_func(b, 1)
Bedienerüberlastung
Ein sehr frisches Thema, weil Wenn Bediener mit voller Sicherheit überlastet werden, verschwindet der ganze Spaß. Diese Frage ist im MyPy-Bug-Tracker wiederholt aufgetaucht, schwört jedoch an einigen Stellen, und Sie können sie sicher deaktivieren.
Ich erkläre die Situation:
class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3
Wenn die zusammengesetzte Zuweisungsmethode NotImplemented
, sucht Python zuerst nach __radd__
, verwendet dann __add__
und voila.
Gleiches gilt für das Überladen von Unterklassenmethoden des Formulars:
class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ...
An einigen Stellen wurden bereits Warnungen in die Dokumentation verschoben, an einigen Stellen arbeiten sie noch am Produkt. Die allgemeine Schlussfolgerung der Mitwirkenden ist jedoch, solche Überlastungen akzeptabel zu lassen.