Python 3.8, das im Oktober 2019 veröffentlicht wurde, brachte eine Vielzahl von Verbesserungen, die Entwickler auf der ganzen Welt begeisterten. Zu den herausragenden Funktionen gehören Zuweisungsausdrücke und Nur-Positions-Argumente. Eine der leistungsstärksten und dennoch unterschätzten Neuerungen sind jedoch die Python-Protokolle, auch bekannt als statische Duck-Typisierung. Aber was genau sind Python-Protokolle und wie können sie Ihre Codierungspraktiken verbessern?
Um Ihnen einen guten Überblick darüber zu geben, wo Protokolle eingesetzt werden und warum sie nützlich sind, werde ich zunächst die folgenden Themen besprechen:
- Dynamische versus statische Typisierung
- Typ-Hinweise
- ABCs
- Und schließlich: Protokolle
Dynamische versus statische Typisierung und statische Typüberprüfung
Python ist eine dynamisch typisierte Sprache. Was soll das bedeuten?
Erstens: Typendeklarationen sind nicht erforderlich. Ich kann die folgende Funktion definieren, ohne jemals anzugeben, welche Typen die Argumente haben sollen, und ich muss auch keinen Rückgabetyp angeben:
def my_function(a, b, c):
return a + b - c
KOPIEREN
Python
Kopieren Sie
Zweitens: Typen werden zur Laufzeit gehandhabt - und überprüft -. Ich kann my_function entweder mit Ganzzahlen, Fließkommazahlen oder einer Mischung aus beidem als Eingabe starten. Der Rückgabetyp hängt von der Eingabe ab:
result = my_function(5, 3, 2)
# type(result) -> int
result = my_function(5.1, 3, 2)
# type(result) -> float
KOPIEREN
Python
Kopieren Sie
Wenn wir dies mit C vergleichen, einer statisch typisierten Sprache, sehen wir, dass wir Typendeklarationen bereitstellen müssen:
int my_function(int a, int b, int c) { return a + b - c; }
KOPIEREN
C
Kopieren Sie
Die Angabe eines anderen Typs wäre illegal. Das Folgende würde nicht kompiliert:
int result = my_function(5.1, 3, 2);
KOPIEREN
C
Kopieren Sie
Das ist ein Vorteil einer statisch typisierten Sprache: Die Typen werden beim Kompilieren überprüft, so dass Sie zur Laufzeit keine Probleme mit den Typen bekommen können. In Python können Sie zur Laufzeit auf Probleme stoßen, die Sie in einer statisch typisierten Sprache nie hätten. Andererseits sind dynamisch typisierte Sprachen flexibler, wenn es um die akzeptierten Typen geht. Und sie erfordern keine Typendeklarationen, was für faule Programmierer großartig ist.
Duck Typing
Die dynamische Typisierung wird auch Enten-Typisierung genannt, weil
Wenn es wie eine Ente läuft und wie eine Ente quakt, dann muss es eine Ente sein.
Oder anders ausgedrückt: Wenn ein Objekt die erforderliche Funktionalität besitzt, sollten wir es als Argument akzeptieren.
Nehmen wir zum Beispiel an, dass wir eine Klasse namens Duck haben und diese Klasse kann laufen und quaken:
class Duck:
def walk(self):
...
def quack(self):
...
KOPIEREN
Python
Kopieren Sie
Dann können wir eine Instanz dieser Klasse erzeugen und sie laufen und quaken lassen:
duck = Duck()
duck.walk()
duck.quack()
Python
Wenn wir nun einen Esel der Klasse Esel haben, der laufen kann, aber nicht quaken kann:
class Donkey:
def walk(self):
...
Python
Wenn wir dann versuchen, eine Instanz des Esels zum Laufen und Quaken zu bringen:
duck = Donkey()
duck.walk()
duck.quack()
KOPIEREN
Python
Kopieren Sie
Wir erhalten eine >> AttributeError: Das Objekt 'Esel' hat kein Attribut 'quack'. Beachten Sie, dass wir dies nur zur Laufzeit erhalten !
Wir können die Ente jedoch durch jede andere Klasse ersetzen, die laufen und quaken kann. Zum Beispiel:
class ImpostorDuck:
def walk(self):
...
def quack(self):
not_quite_quacking()
duck = ImpostorDuck()
duck.walk()
duck.quack()
KOPIEREN
Python
Kopieren Sie
Also, zusammenfassend: Python ist eine dynamisch typisierte Sprache. Das ist großartig, denn es bietet viel Flexibilität und Typendeklarationen sind nicht erforderlich. Allerdings findet außer zur Laufzeit keine Typüberprüfung statt, was zu unerwarteten Problemen führen kann. Das führt uns zu...
Typ-Hinweise
Type Hints, oder optionale statische Typisierung, wurden in Python 3.5 eingeführt, um diesen Nachteil zu überwinden. Damit können Sie optional Typen von Argumenten und Rückgabewerten angeben, die dann von einem statischen Typprüfer wie mypy überprüft werden können.
Das Typisierungsmodul spielt eine entscheidende Rolle bei der Definition von Typ-Hinweisen und Protokollen, mit denen Entwickler die Typen von Variablen, Funktionsargumenten und Rückgabewerten angeben können.
Nehmen wir zum Beispiel an, wir haben einen Typ Duck, der schwimmen und Brot essen kann: class Duck: def eat_bread(self): ...
def swim(self):
...
Python
Wir können dann eine Funktion feed_bread definieren, die eine Ente Brot essen lässt. Wir können den Typ des Arguments auf den Typ Duck festlegen def feed_the_duck(duck: Duck): duck.eat_bread()
duck = Duck() feed_the_duck(duck) COPY
Python
Kopieren Sie
Wenn Sie nun zum Beispiel versuchen, einen Affen mit Brot zu füttern, wird das nicht funktionieren: class Monkey: def eat_bananas(self): ...
def climb_tree(self):
...
monkey = Affe() feed_the_duck(monkey) Python
Zur Laufzeit erhalten Sie dann: >> AttributeError: Das Objekt 'Monkey' hat kein Attribut 'eat_bread'.
Aber mypy kann Probleme wie diese erkennen, bevor Sie Ihren Code ausführen. In diesem Fall wird es Ihnen sagen: error: Argument 1 zu "feed_the_duck" hat den inkompatiblen Typ "Monkey"; erwartet wurde "Duck" Diese Typ-Hinweise können Ihr Leben als Entwickler sehr viel einfacher machen, aber sie sind nicht perfekt. Wenn wir zum Beispiel feed_bread generischer machen wollen, so dass es auch andere Tierarten akzeptieren kann, müssen wir alle akzeptierten Typen explizit auflisten: from typing import Union
class Pig: def eat_bread(self): pass
def feed_bread(animal: Union[Duck, Pig]): animal.eat_bread() Python
Und noch ein Nachteil: Wenn Sie wie oben Code verwenden, der von einem externen Paket bereitgestellt wird, das nicht unter Ihrer Kontrolle steht (nehmen wir an, es heißt animals), können Sie ihn nicht für Ihre eigenen Typen verwenden. Die Lieblingsbeschäftigungen meines kleinen Sohnes Mees sind zum Beispiel Brot essen und Milch trinken: animals feed_bread
class Mees: def eat_bread(self): pass
def drink_milk(self):
pass
mees Mees() feed_breadmees COPY
Python
Kopieren Sie
Zur Laufzeit wird der obige Code einwandfrei funktionieren, aber mypy wird sich beschweren:
Fehler: Argument 1 zu "feed_bread" hat den inkompatiblen Typ "Mees"; erwartet wurde "Union[Duck, Pig]" Wenn wir keine Kontrolle über das Paket animals haben, können wir nichts dagegen tun - außer mypy anzuweisen, die entsprechende Zeile zu ignorieren.
Zusammenfassend lässt sich also sagen: Typ-Hinweise sind großartig, weil sie Ihnen die Möglichkeit geben, eine statische Typüberprüfung durchzuführen. Sie sind zwar nicht verpflichtet, Typendeklarationen hinzuzufügen, aber wenn Sie dies tun, erhalten Sie einige der Vorteile einer statisch typisierten Sprache. Aber die Unfähigkeit, die Typhinweise von importiertem Code anzupassen, führt zu Konflikten zwischen der dynamischen Typisierung von Python und den statischen Typhinweisen. Statische Analysewerkzeuge wie mypy können helfen, indem sie die Typkorrektheit überprüfen und Probleme vor der Laufzeit erkennen.
ABCs und abstrakte Methoden
Abstrakte Basisklassen nehmen dem oben beschriebenen Konflikt etwas von seinem Schrecken. Wie der Name schon sagt, handelt es sich um Basisklassen - Klassen, von denen Sie erben sollen -, die aber nicht instanziiert werden können. Sie werden verwendet, um die Schnittstelle zu definieren, wie die Unterklassen des ABCs aussehen sollen.
Ein Beispiel (und verzeihen Sie mir, dass ich davon ausgehe, dass alle Tiere laufen können): class Animal(metaclass=ABCMeta): @abstractmethod def walk(self): pass # Benötigt Implementierung durch Unterklasse COPY
Python
Copy
Instantiating this class is impossible: my_animal = Animal() will yield >> TypeError: Can’t instantiate abstract class Animal with abstract methods walk.
However, if we define a subclass, we can instantiate it: class Duck(Animal): def walk(self): ..
duck = Duck() https://essentials.xebia.com/assertions/ isinstance(duck, Animal) # <-- True COPY
Python
Copy
Als praktisches Beispiel können Sie ein ABC mit dem Namen EatsBread erstellen, das definiert, dass seine Unterklassen tatsächlich Brot essen können (oder, mit anderen Worten, dass sie eine Methode mit der Signatur eat_bread(self) haben müssen): from abc import ABCMeta, abstractmethod
class EatsBread(metaclass=ABCMeta): @abstractmethod def eat_bread(self): pass
class Duck(EatsBread): def eat_bread(self): ..
class Pig(EatsBread): def eat_bread(self): ...
def feed_bread(animal: EatsBread): animal.eat_bread() Now if I were to use this implementation of feed_bread in my code of Mees – I can make Mees a subclass of EatsBread and all will be fine: from animals import EatsBread, feed_bread
class Mees(EatsBread): def eat_bread(self): ...
def drink_milk(self):
...
feed_bread(Mees()) # <-- OK at runtime and for mypy Python
Obwohl dies viel besser ist, ist es immer noch nicht perfekt. Oft sind Basisklassen nicht einfach offengelegt, was bedeutet, dass ich hässliche Importe verwenden muss, um zu bekommen, was ich brauche: from animals import feed_bread from animals.base.eats import EatsBread Python
Außerdem müssen Sie entweder von der Basisklasse erben (oder Ihre Klasse explizit als Unterklasse registrieren, z.B. EatsBread.register(Mees)), damit dies funktioniert - was nicht so schön ist wie das implizite Verhalten von Duck Typing.
Und dennoch kann es Situationen geben, die nicht ganz funktionieren. Nehmen wir an, wir verwenden zwei externe Pakete:
Aus package animals: class Animal(metaclass=ABCMeta): @abstractmethod def walk(self): pass
class Dog(Animal): def walk(self): ...
def walk_animal(animal: Tier): animal.walk() Python
Und aus package llamas: class Llama: def walk(self): ... KOPIEREN
Python
Kopieren Sie
Wenn Sie diese nun in Ihrem Code kombinieren: from animals import walk_animal from llamas import Llama
llama = Llama() walk_animal(llama) # <-- Nicht OK für mypy COPY
Python
Kopieren Sie
Diese letzte Zeile wird zur Laufzeit funktionieren - aber da Llama nicht von Animal erbt, wird sich mypy beschweren.
Man kann dieses Problem lösen, indem man Llama zu einer virtuellen Unterklasse von Animal macht: Animal.register(Llama)
llama = Llama() walk_animal(llama) # <-- OK COPY
Python
Kopieren Sie
Aber ich würde sagen, dass das alles andere als schön ist.
Ich fasse noch einmal zusammen: ABCs geben Ihren Typen Struktur, was großartig ist. Das bedeutet, dass Typ-Hinweise für neue Unterklassen nicht aktualisiert werden müssen. Aber wir können immer noch Probleme haben, wenn wir Klassen aus mehreren Paketen kombinieren. Und all diese Typisierung - sei es durch Vererbung oder als virtuelle Unterklassen - muss explizit erfolgen, was im Widerspruch zur dynamischen und impliziten Natur der dynamischen Typisierung steht. ABCs schränken auch die Mehrfachvererbung ein, was in einigen Szenarien restriktiv sein kann.
Wenn Sie mehrere Protokolle verwenden, kann eine Klasse explizit von mehreren Protokollen erben und Methoden mit normaler MRO auflösen. Dadurch wird sichergestellt, dass die Typüberprüfung die korrekte Subtypisierung überprüft, was im Vergleich zu ABCs eine flexiblere Lösung darstellt.
Python-Protokolle und Protokollmethoden
Und genau hier kommen Protokolle ins Spiel. Ein Protokoll ist ein Spezialfall von ABC, der implizit funktioniert: from typing import Protocol
class EatsBread(Protocol): eat_bread(self): pass
def feed_bread(animal: EatsBread): animal.eat_bread()
class Duck: def eat_breadself ...
feed_breadDuck # <-- OK Im obigen Code wird Duck implizit als ein Subtyp von EatsBread betrachtet. Es besteht keine Notwendigkeit, in der Klassendefinition explizit vom Protokoll zu erben. Jede Klasse, die alle im Protokoll definierten Attribute und Methoden (mit übereinstimmenden Signaturen) implementiert, wird als Untertyp dieses Protokolls angesehen.
Wenn wir also die Funktion feed_bread aus dem Paket animals verwenden würden: from animals import feed_bread
class Mees: def eat_bread(self): ...
def drink_milk(self):
...
feed_bread(Mees()) # <-- OK Python
Auch hier ist Mees implizit ein Untertyp von EatsBread. Auch hier ist es nicht nötig, dies explizit anzugeben: Solange die Signaturen übereinstimmen, funktioniert es einfach! Aus diesem Grund werden Protokolle auch als statische Typisierung von Enten bezeichnet. Darüber hinaus können Klassenobjekte für Introspektion und Typhinweise verwendet werden, um die Kompatibilität mit Protokollmitgliedern zu bestimmen.
Beachten Sie, dass all dies nur während der Typüberprüfung funktioniert: nicht zur Laufzeit! Wenn Sie möchten, dass dies zur Laufzeit funktioniert, können Sie den runtime_checkable-decorator verwenden
Der Körper der Protokollklasse ist der Ort, an dem alle in einem Protokoll definierten Methoden als Protokollmitglieder betrachtet werden, einschließlich normaler und dekorierter Methoden. Hier werden auch die Körper der Protokollmethoden auf ihren Typ hin überprüft.
Ein letztes Mal: Protokolle sind großartig, denn:
- Es ist nicht notwendig, explizit von einem Protokoll zu erben oder Ihre Klasse als virtuelle Unterklasse zu registrieren.
- Es gibt keine Schwierigkeiten mehr, Pakete zu kombinieren: Es funktioniert, solange die Signaturen übereinstimmen.
- Wir haben jetzt das Beste aus beiden Welten: statische Typüberprüfung von dynamischen Typen.
Die Protokollklassen-Implementierung definiert Regeln und Richtlinien für die Implementierung von Protokollmethoden innerhalb einer Klasse, einschließlich der Verwendung von Variablen-Annotationen.
Python Protokoll-Klassenvariablen werden innerhalb des Klassenkörpers eines Protokolls definiert, und es wird zwischen Protokoll-Klassenvariablen und Protokoll-Instanzvariablen unterschieden.
Protokollklassen verhalten sich im Rahmen der statischen Typüberprüfung und strukturellen Subtypisierung ähnlich wie reguläre Klassen.
Die Instanzvariablen des Python-Protokolls werden innerhalb des Klassenkörpers des Protokolls definiert und verwendet, mit spezifischen Anmerkungen und Regeln.
Die Methoden des Python-Protokolls umfassen statische Methoden, Klassenmethoden und Eigenschaften als zulässige Protokollmitglieder.
FAQs
Was ist der größte Vorteil der Verwendung von Python-Protokollen? Protokolle bieten eine flexible Möglichkeit, eine statische Typüberprüfung durchzuführen, ohne dass eine explizite Vererbung oder Registrierung erforderlich ist, und bieten somit die Vorteile sowohl der statischen als auch der dynamischen Typisierung.
Wie unterscheiden sich Protokolle von abstrakten Basisklassen (ABCs)? Anders als ABCs erfordern Protokolle keine explizite Vererbung. Jede Klasse, die die erforderlichen Methoden und Attribute implementiert, kann als Untertyp des Protokolls betrachtet werden.
Können Python-Protokolle zur Laufzeit verwendet werden? Standardmäßig werden Protokolle nur zur Kompilierzeit geprüft. Sie können jedoch den Dekorator runtime_checkable verwenden, um Protokolle zur Laufzeit durchsetzbar zu machen.
Was ist statische Duck-Typisierung? Die statische Duck-Typisierung, die durch Protokolle ermöglicht wird, erlaubt es Python, die Typüberprüfung auf der Grundlage der Struktur und der Methoden eines Objekts und nicht auf der Grundlage seiner expliziten Klassenvererbung durchzuführen.
Wie verbessern Protokolle die Flexibilität des Codes? Protokolle ermöglichen es, dass verschiedene Klassen mit Funktionen oder Methoden kompatibel sind, solange sie die erforderlichen Methoden implementieren, was die Wiederverwendung von Code und die Flexibilität fördert.
Haben Protokolle Auswirkungen auf die Leistung von Python-Code? Protokolle wirken sich in erster Linie auf die Typprüfung während der Entwicklung aus und haben keinen wesentlichen Einfluss auf die Laufzeitleistung von Python-Code.
Verfasst von
Rogier van der Geer
Unsere Ideen
Weitere Blogs
Contact



