PEP 443 – Funktionen mit einfacher Weiterleitung
- Autor:
- Łukasz Langa <lukasz at python.org>
- Discussions-To:
- Python-Dev Liste
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 22. Mai 2013
- Python-Version:
- 3.4
- Post-History:
- 22. Mai 2013, 25. Mai 2013, 31. Mai 2013
- Ersetzt:
- 245, 246, 3124
Zusammenfassung
Diese PEP schlägt einen neuen Mechanismus im Standardbibliotheksmodul functools vor, der eine einfache Form von generischer Programmierung bereitstellt, bekannt als Funktionen mit einfacher Weiterleitung.
Eine generische Funktion besteht aus mehreren Funktionen, die die gleiche Operation für verschiedene Typen implementieren. Welche Implementierung während eines Aufrufs verwendet werden soll, wird durch den Weiterleitungsalgorithmus bestimmt. Wenn die Implementierung basierend auf dem Typ eines einzelnen Arguments ausgewählt wird, nennt man dies einfache Weiterleitung.
Begründung und Ziele
Python bietet schon immer eine Vielzahl von eingebauten und standardmäßigen generischen Funktionen, wie z. B. len(), iter(), pprint.pprint(), copy.copy() und die meisten Funktionen im Modul operator. Derzeit gibt es jedoch
- keine einfache oder unkomplizierte Möglichkeit für Entwickler, neue generische Funktionen zu erstellen,
- keine standardmäßige Möglichkeit, Methoden zu bestehenden generischen Funktionen hinzuzufügen (d. h. einige werden über Registrierungsfunktionen hinzugefügt, andere erfordern die Definition von
__special__-Methoden, möglicherweise durch Monkeypatching).
Darüber hinaus ist es derzeit ein gängiges Antipattern, in Python-Code den Typ von empfangenen Argumenten zu überprüfen, um zu entscheiden, was mit den Objekten geschehen soll.
Beispielsweise möchte Code möglicherweise entweder ein Objekt eines bestimmten Typs oder eine Sequenz von Objekten dieses Typs akzeptieren. Derzeit ist der „offensichtliche Weg“ hierfür die Typeninspektion, aber das ist fehleranfällig und nicht erweiterbar.
Abstrakte Basisklassen erleichtern die Erkennung vorhandenen Verhaltens, helfen aber nicht beim Hinzufügen neuen Verhaltens. Ein Entwickler, der eine bereits geschriebene Bibliothek verwendet, kann möglicherweise nicht ändern, wie seine Objekte von einem solchen Code behandelt werden, insbesondere wenn die von ihm verwendeten Objekte von einem Drittanbieter erstellt wurden.
Daher schlägt diese PEP eine einheitliche API vor, um dynamische Überladung mithilfe von Decorators zu adressieren.
Benutzer-API
Um eine generische Funktion zu definieren, dekorieren Sie sie mit dem Decorator @singledispatch. Beachten Sie, dass die Weiterleitung auf dem Typ des ersten Arguments erfolgt. Erstellen Sie Ihre Funktion entsprechend
>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
... if verbose:
... print("Let me just say,", end=" ")
... print(arg)
Um überladene Implementierungen zur Funktion hinzuzufügen, verwenden Sie das Attribut register() der generischen Funktion. Dies ist ein Decorator, der einen Typ-Parameter entgegennimmt und eine Funktion dekoriert, die die Operation für diesen Typ implementiert
>>> @fun.register(int)
... def _(arg, verbose=False):
... if verbose:
... print("Strength in numbers, eh?", end=" ")
... print(arg)
...
>>> @fun.register(list)
... def _(arg, verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
Um die Registrierung von Lambdas und bereits vorhandenen Funktionen zu ermöglichen, kann das Attribut register() in funktionaler Form verwendet werden
>>> def nothing(arg, verbose=False):
... print("Nothing.")
...
>>> fun.register(type(None), nothing)
Das Attribut register() gibt die undekorierte Funktion zurück. Dies ermöglicht Decorator-Stacking, Pickling sowie die Erstellung von Unit-Tests für jede Variante unabhängig
>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
... if verbose:
... print("Half of your number:", end=" ")
... print(arg / 2)
...
>>> fun_num is fun
False
Beim Aufruf leitet die generische Funktion auf dem Typ des ersten Arguments weiter
>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615
Wenn keine registrierte Implementierung für einen bestimmten Typ vorhanden ist, wird seine Method Resolution Order (MRO) verwendet, um eine allgemeinere Implementierung zu finden. Die ursprüngliche Funktion, die mit @singledispatch dekoriert wurde, wird für den Basis-Typ object registriert, was bedeutet, dass sie verwendet wird, wenn keine bessere Implementierung gefunden wird.
Um zu überprüfen, welche Implementierung die generische Funktion für einen bestimmten Typ wählen wird, verwenden Sie das Attribut dispatch()
>>> fun.dispatch(float)
<function fun_num at 0x104319058>
>>> fun.dispatch(dict) # note: default implementation
<function fun at 0x103fe0000>
Um auf alle registrierten Implementierungen zuzugreifen, verwenden Sie das schreibgeschützte Attribut registry
>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
<class 'decimal.Decimal'>, <class 'list'>,
<class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>
Die vorgeschlagene API ist bewusst begrenzt und meinungsstark, um sicherzustellen, dass sie leicht zu erklären und zu verwenden ist, und um die Konsistenz mit bestehenden Mitgliedern des Moduls functools zu gewährleisten.
Implementierungs-Hinweise
Die in dieser PEP beschriebene Funktionalität ist bereits im Standardbibliotheksmodul pkgutil als simplegeneric implementiert. Da diese Implementierung ausgereift ist, besteht das Ziel darin, sie weitgehend unverändert zu übernehmen. Die Referenzimplementierung ist auf hg.python.org verfügbar [1].
Der Weiterleitungstyp wird als Argument des Decorators angegeben. Eine alternative Form mit Funktionsannotationen wurde in Betracht gezogen, aber ihre Aufnahme wurde abgelehnt. Zum Zeitpunkt Mai 2013 fällt dieses Nutzungsmuster nicht in den Geltungsbereich der Standardbibliothek [2], und die Best Practices für die Verwendung von Annotationen werden noch diskutiert.
Basierend auf der aktuellen Implementierung von pkgutil.simplegeneric und im Einklang mit der Konvention zur Registrierung von virtuellen Unterklassen auf abstrakten Basisklassen ist die Weiterleitungsregistrierung nicht threadsicher.
Abstrakte Basisklassen
Die Implementierung von pkgutil.simplegeneric stützte sich auf verschiedene Formen der Method Resolution Order (MRO). @singledispatch entfernt die spezielle Behandlung von Klassen alten Stils und von Zope’s ExtensionClasses. Wichtiger ist, dass es Unterstützung für Abstrakte Basisklassen (ABC) einführt.
Wenn eine Implementierung einer generischen Funktion für eine ABC registriert wird, wechselt der Weiterleitungsalgorithmus zu einer erweiterten Form der C3-Linearisierung, die die relevanten ABCs in die MRO des bereitgestellten Arguments einbezieht. Der Algorithmus fügt ABCs dort ein, wo ihre Funktionalität eingeführt wird, d. h. issubclass(cls, abc) gibt True für die Klasse selbst zurück, aber False für alle ihre direkten Basisklassen. Implizite ABCs für eine gegebene Klasse (entweder registriert oder abgeleitet aus dem Vorhandensein einer speziellen Methode wie __len__()) werden direkt nach der letzten ABC eingefügt, die explizit in der MRO der betreffenden Klasse aufgeführt ist.
In seiner grundlegendsten Form gibt diese Linearisierung die MRO für den gegebenen Typ zurück
>>> _compose_mro(dict, [])
[<class 'dict'>, <class 'object'>]
Wenn das zweite Argument ABCs enthält, von denen der angegebene Typ eine Unterklasse ist, werden diese in einer vorhersagbaren Reihenfolge eingefügt
>>> _compose_mro(dict, [Sized, MutableMapping, str,
... Sequence, Iterable])
[<class 'dict'>, <class 'collections.abc.MutableMapping'>,
<class 'collections.abc.Mapping'>, <class 'collections.abc.Sized'>,
<class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>,
<class 'object'>]
Obwohl dieser Betriebsmodus erheblich langsamer ist, werden alle Weiterleitungsentscheidungen zwischengespeichert. Der Cache wird ungültig gemacht, wenn neue Implementierungen für die generische Funktion registriert werden oder wenn benutzerdefinierter Code register() auf einer ABC aufruft, um diese implizit zu unterklassen. Im letzteren Fall ist es möglich, eine Situation mit mehrdeutiger Weiterleitung zu schaffen, zum Beispiel
>>> from collections.abc import Iterable, Container
>>> class P:
... pass
>>> Iterable.register(P)
<class '__main__.P'>
>>> Container.register(P)
<class '__main__.P'>
Angesichts von Mehrdeutigkeiten vermeidet @singledispatch die Versuchung zu raten
>>> @singledispatch
... def g(arg):
... return "base"
...
>>> g.register(Iterable, lambda arg: "iterable")
<function <lambda> at 0x108b49110>
>>> g.register(Container, lambda arg: "container")
<function <lambda> at 0x108b491c8>
>>> g(P())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>
Beachten Sie, dass diese Ausnahme nicht ausgelöst würde, wenn eine oder mehrere ABCs während der Klassendefinition explizit als Basisklassen angegeben worden wären. In diesem Fall erfolgt die Weiterleitung in der MRO-Reihenfolge
>>> class Ten(Iterable, Container):
... def __iter__(self):
... for i in range(10):
... yield i
... def __contains__(self, value):
... return value in range(10)
...
>>> g(Ten())
'iterable'
Ein ähnlicher Konflikt entsteht, wenn die Unterklasse einer ABC aus dem Vorhandensein einer speziellen Methode wie __len__() oder __contains__() abgeleitet wird
>>> class Q:
... def __contains__(self, value):
... return False
...
>>> issubclass(Q, Container)
True
>>> Iterable.register(Q)
>>> g(Q())
Traceback (most recent call last):
...
RuntimeError: Ambiguous dispatch: <class 'collections.abc.Container'>
or <class 'collections.abc.Iterable'>
Eine frühe Version der PEP enthielt einen benutzerdefinierten Ansatz, der einfacher war, aber eine Reihe von Randfällen mit überraschenden Ergebnissen erzeugte [3].
Anwendungsmuster
Diese PEP schlägt vor, nur das Verhalten von Funktionen zu erweitern, die explizit als generisch gekennzeichnet sind. So wie eine Basisklassenmethode von einer Unterklasse überschrieben werden kann, so kann auch eine Funktion überladen werden, um benutzerdefiniertes Verhalten für einen bestimmten Typ bereitzustellen.
Universelle Überladung ist nicht gleichbedeutend mit *beliebiger* Überladung, in dem Sinne, dass wir nicht erwarten müssen, dass Leute das Verhalten bestehender Funktionen auf zufällige und unvorhersehbare Weise neu definieren. Im Gegenteil, die Verwendung von generischen Funktionen in tatsächlichen Programmen folgt tendenziell sehr vorhersagbaren Mustern, und registrierte Implementierungen sind im Allgemeinen leicht auffindbar.
Wenn ein Modul eine neue generische Operation definiert, wird es normalerweise auch alle erforderlichen Implementierungen für bestehende Typen an derselben Stelle definieren. Ebenso, wenn ein Modul einen neuen Typ definiert, wird es dort normalerweise Implementierungen für alle generischen Funktionen definieren, die ihm bekannt sind oder um die es sich kümmert. Infolgedessen kann die überwiegende Mehrheit der registrierten Implementierungen entweder neben der überladenen Funktion oder neben einem neu definierten Typ, für den die Implementierung Unterstützung hinzufügt, gefunden werden.
Nur in eher seltenen Fällen werden Implementierungen in einem Modul registriert, das weder die Funktion noch den Typ (bzw. die Typen) enthält, für die die Implementierung hinzugefügt wird. In Abwesenheit von Inkompetenz oder absichtlicher Verschleierung werden die wenigen Implementierungen, die nicht neben den relevanten Typen oder Funktionen registriert sind, im Allgemeinen nicht außerhalb des Geltungsbereichs, in dem diese Implementierungen definiert sind, verstanden oder bekannt sein müssen. (Außer im Fall von „Support-Modulen“, wo die Best Practices vorschlagen, diese entsprechend zu benennen.)
Wie bereits erwähnt, sind Single-Dispatch-Generics in der Standardbibliothek bereits weit verbreitet. Eine saubere, standardmäßige Methode, dies zu tun, bietet einen Weg, diese benutzerdefinierten Implementierungen zu refaktorisieren, um eine gemeinsame zu verwenden, und sie gleichzeitig für Benutzererweiterungen zu öffnen.
Alternative Ansätze
In PEP 3124 schlägt Phillip J. Eby eine ausgereifte Lösung mit Überladung basierend auf beliebigen Regelwerken vor (wobei die Standardimplementierung auf Argumenttypen weiterleitet), sowie Schnittstellen, Adaption und Methodenkombination. PEAK-Rules [4] ist eine Referenzimplementierung der in PJE’s PEP beschriebenen Konzepte.
Ein derart breiter Ansatz ist von Natur aus komplex, was die Einigung erschwert. Im Gegensatz dazu konzentriert sich diese PEP auf ein einzelnes Funktionsmerkmal, das einfach zu verstehen ist. Es ist wichtig zu beachten, dass dies die Verwendung anderer Ansätze jetzt oder in Zukunft nicht ausschließt.
In einem Artikel aus dem Jahr 2005 auf Artima [5] präsentiert Guido van Rossum eine Implementierung generischer Funktionen, die auf die Typen aller Argumente einer Funktion weiterleitet. Der gleiche Ansatz wurde in Andrey Popps generic-Paket auf PyPI [6] sowie in David Mertz’ gnosis.magic.multimethods [7] gewählt.
Obwohl dies auf den ersten Blick wünschenswert erscheint, stimme ich Fredrik Lundhs Kommentar zu, dass „wenn Sie APIs mit Seiten von Logik entwerfen, nur um zu sortieren, welcher Code eine Funktion ausführen soll, Sie wahrscheinlich das API-Design an jemand anderen übergeben sollten“. Mit anderen Worten, der in dieser PEP vorgeschlagene Ansatz mit einem einzelnen Argument ist nicht nur einfacher zu implementieren, sondern kommuniziert auch klar, dass die Weiterleitung auf einen komplexeren Zustand ein Antipattern ist. Er hat auch den Vorteil, dass er direkt dem vertrauten Mechanismus der Methodenweiterleitung in der objektorientierten Programmierung entspricht. Der einzige Unterschied besteht darin, ob die benutzerdefinierte Implementierung näher an den Daten (objektorientierte Methoden) oder am Algorithmus (Single-Dispatch-Überladung) assoziiert ist.
PyPys RPython bietet extendabletype [8], eine Metaklasse, die es Klassen ermöglicht, extern erweitert zu werden. In Kombination mit den Factory-Funktionen pairtype() und pair() bietet dies eine Form von Single-Dispatch-Generics.
Danksagungen
Neben Phillip J. Ebys Arbeit an PEP 3124 und PEAK-Rules sind Einflüsse Paul Moores ursprüngliche Ausgabe [9], die die Freigabe von pkgutil.simplegeneric als Teil der functools API vorschlug, Guido van Rossums Artikel über Multimethoden [5] und Diskussionen mit Raymond Hettinger über eine allgemeine pprint-Überarbeitung. Vielen Dank an Alyssa Coghlan, die mich ermutigt hat, diese PEP zu erstellen, und mir erstes Feedback gegeben hat.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Source: https://github.com/python/peps/blob/main/peps/pep-0443.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT