Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 3124 – Überladung, generische Funktionen, Schnittstellen und Anpassung

Autor:
Phillip J. Eby <pje at telecommunity.com>
Discussions-To:
Python-3000 Liste
Status:
Verschoben
Typ:
Standards Track
Benötigt:
3107, 3115, 3119
Erstellt:
28. Apr. 2007
Post-History:
30. Apr. 2007
Ersetzt:
245, 246

Inhaltsverzeichnis

Verschoben

Siehe https://mail.python.org/pipermail/python-3000/2007-July/008784.html.

Zusammenfassung

Dieser PEP schlägt ein neues Modul der Standardbibliothek vor, overloading, um generische Programmierfunktionen bereitzustellen, darunter dynamische Überladung (auch bekannt als generische Funktionen), Schnittstellen, Anpassung, Methodenkombination (wie bei CLOS und AspectJ) und einfache Formen der aspektorientierten Programmierung (AOP).

Die vorgeschlagene API ist auch erweiterbar; das heißt, es wird für Bibliotheksentwickler möglich sein, ihre eigenen spezialisierten Schnittstellentypen, generischen Funktions-Dispatcher, Algorithmen zur Methodenkombination usw. zu implementieren, und diese Erweiterungen werden von der vorgeschlagenen API als First-Class-Bürger behandelt.

Die API wird in reinem Python ohne C implementiert, kann aber einige Abhängigkeiten von CPython-spezifischen Funktionen wie sys._getframe und dem Attribut func_code von Funktionen haben. Es wird erwartet, dass z. B. Jython und IronPython andere Möglichkeiten haben werden, ähnliche Funktionalität zu implementieren (vielleicht unter Verwendung von Java oder C#).

Begründung und Ziele

Python hat schon immer eine Vielzahl von integrierten und Standardbibliotheks-generischen Funktionen bereitgestellt, wie len(), iter(), pprint.pprint() und die meisten Funktionen im Modul operator. Jedoch hat es derzeit

  1. keine einfache oder unkomplizierte Möglichkeit für Entwickler, neue generische Funktionen zu erstellen,
  2. keine standardmäßige Methode, um 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 Monkey-Patching), und
  3. ermöglicht keine Dispatching auf Basis mehrerer Argumenttypen (außer in begrenzter Form für arithmetische Operatoren, wo "rechte" (__r*__) Methoden verwendet werden können, um Two-Argument-Dispatching durchzuführen.

Darüber hinaus ist es derzeit ein gängiges Antipattern, dass Python-Code die Typen empfangener Argumente inspiziert, um zu entscheiden, was mit den Objekten geschehen soll. Beispielsweise kann Code entweder ein Objekt eines bestimmten Typs oder eine Sequenz von Objekten dieses Typs akzeptieren wollen.

Derzeit ist der "offensichtliche Weg" dafür die Typeninspektion, aber das ist fehleranfällig und nicht erweiterbar. Ein Entwickler, der eine bereits geschriebene Bibliothek verwendet, kann möglicherweise nicht ändern, wie seine Objekte von solchem Code behandelt werden, insbesondere wenn die verwendeten Objekte von einem Drittanbieter erstellt wurden.

Daher schlägt dieser PEP ein Modul der Standardbibliothek vor, um diese und verwandte Probleme mit Hilfe von Dekoratoren und Argumentannotationen (PEP 3107) zu lösen. Die wichtigsten Funktionen, die bereitgestellt werden sollen, sind:

  • eine dynamische Überladungseinrichtung, ähnlich der statischen Überladung in Sprachen wie Java und C++, aber einschließlich optionaler Methodenkombinationsfunktionen, wie bei CLOS und AspectJ.
  • eine einfache "Interfaces und Anpassung"-Bibliothek, inspiriert von Haskells Typklassen (aber dynamischer und ohne statische Typüberprüfung), mit einer Erweiterungs-API, um benutzerdefinierte Schnittstellentypen zuzulassen, wie sie in PyProtocols und Zope vorkommen.
  • eine einfache "Aspect"-Implementierung, um die Erstellung von zustandsbehafteten Adaptern und andere zustandsbehaftete AOP zu erleichtern.

Diese Funktionen sollen so bereitgestellt werden, dass erweiterte Implementierungen erstellt und verwendet werden können. Es soll beispielsweise möglich sein, dass Bibliotheken neue Dispatch-Kriterien für generische Funktionen und neue Arten von Schnittstellen definieren und diese anstelle der vordefinierten Funktionen verwenden. Es soll beispielsweise möglich sein, ein zope.interface-Schnittstellenobjekt zu verwenden, um den gewünschten Typ eines Funktionsarguments anzugeben, solange das zope.interface-Paket sich selbst korrekt registriert hat (oder ein Dritter die Registrierung vorgenommen hat).

Auf diese Weise bietet die vorgeschlagene API einfach einen einheitlichen Weg, auf die Funktionalität in ihrem Geltungsbereich zuzugreifen, anstatt eine einzelne Implementierung vorzuschreiben, die für alle Bibliotheken, Frameworks und Anwendungen verwendet werden soll.

Benutzer-API

Die Überladungs-API wird als einzelnes Modul namens overloading implementiert, das die folgenden Funktionen bereitstellt:

Überladung/Generische Funktionen

Der Dekorator @overload erlaubt die Definition alternativer Implementierungen einer Funktion, die nach Argumenttyp(en) spezialisiert sind. Eine Funktion mit demselben Namen muss bereits im lokalen Namensraum vorhanden sein. Die vorhandene Funktion wird vom Dekorator inplace modifiziert, um die neue Implementierung hinzuzufügen, und die modifizierte Funktion wird vom Dekorator zurückgegeben. Folgender Code

from overloading import overload
from collections import Iterable

def flatten(ob):
    """Flatten an object to its component iterables"""
    yield ob

@overload
def flatten(ob: Iterable):
    for o in ob:
        for ob in flatten(o):
            yield ob

@overload
def flatten(ob: basestring):
    yield ob

erstellt eine einzelne flatten()-Funktion, deren Implementierung ungefähr Folgendem entspricht:

def flatten(ob):
    if isinstance(ob, basestring) or not isinstance(ob, Iterable):
        yield ob
    else:
        for o in ob:
            for ob in flatten(o):
                yield ob

außer dass die von der Überladung definierte flatten()-Funktion offen für Erweiterungen durch Hinzufügen weiterer Überladungen bleibt, während die fest kodierte Version nicht erweitert werden kann.

Wenn beispielsweise jemand flatten() mit einem zeichenkettenähnlichen Typ verwenden möchte, der nicht von basestring erbt, hätte er bei der zweiten Implementierung Pech. Mit der überladenen Implementierung kann er jedoch entweder Folgendes schreiben:

@overload
def flatten(ob: MyString):
    yield ob

oder dies (um die Implementierung zu vermeiden)

from overloading import RuleSet
RuleSet(flatten).copy_rules((basestring,), (MyString,))

(Beachten Sie auch, dass PEP 3119 vorschlägt, dass abstrakte Basisklassen wie Iterable es Klassen wie MyString ermöglichen sollten, Unterklassenansprüche geltend zu machen, ein solcher Anspruch ist *global* für die gesamte Anwendung. Im Gegensatz dazu ist das Hinzufügen einer spezifischen Überladung oder das Kopieren einer Regel spezifisch für eine einzelne Funktion und daher weniger wahrscheinlich unerwünschte Nebenwirkungen zu haben.)

@overload vs. @when

Der Dekorator @overload ist eine gängige Kurzform für den allgemeineren Dekorator @when. Er erlaubt es, den Namen der zu überladenden Funktion wegzulassen, auf Kosten der Anforderung, dass die Ziel funktion im lokalen Namensraum vorhanden sein muss. Außerdem unterstützt er nicht das Hinzufügen zusätzlicher Kriterien außer denen, die über Argumentannotationen angegeben werden. Die folgenden Funktionsdefinitionen haben identische Auswirkungen, abgesehen von den Nebeneffekten der Namensbindung (die unten beschrieben werden):

from overloading import when

@overload
def flatten(ob: basestring):
    yield ob

@when(flatten)
def flatten(ob: basestring):
    yield ob

@when(flatten)
def flatten_basestring(ob: basestring):
    yield ob

@when(flatten, (basestring,))
def flatten_basestring(ob):
    yield ob

Die erste Definition oben bindet flatten an das, womit es zuvor gebunden war. Die zweite tut dasselbe, wenn sie bereits an das erste Argument des when-Dekorators gebunden war. Wenn flatten ungebunden ist oder an etwas anderes gebunden ist, wird es an die Funktionsdefinition wie gegeben gebunden. Die letzten beiden Definitionen oben binden flatten_basestring immer an die Funktionsdefinition wie gegeben.

Die Verwendung dieses Ansatzes ermöglicht es, einer Methode einen beschreibenden Namen zu geben (oft nützlich in Tracebacks!) und die Methode später wiederzuverwenden.

Sofern nicht anders angegeben, haben alle overloading-Dekoratoren dieselbe Signatur und Bindungsregeln wie @when. Sie akzeptieren eine Funktion und ein optionales "Prädikat"-Objekt.

Die Standardimplementierung des Prädikats ist ein Tupel von Typen mit positiver Übereinstimmung mit den Argumenten der überladenen Funktion. Es können jedoch beliebig viele andere Arten von Prädikaten erstellt und mit der Erweiterungs-API registriert werden und sind dann mit @when und anderen von diesem Modul erstellten Dekoratoren (wie @before, @after und @around) verwendbar.

Methodenkombination und -überschreibung

Wenn eine überladene Funktion aufgerufen wird, wird die Implementierung mit der Signatur verwendet, die die aufrufenden Argumente *am spezifischsten übereinstimmt*. Wenn keine Implementierung übereinstimmt, wird ein NoApplicableMethods-Fehler ausgelöst. Wenn mehr als eine Implementierung übereinstimmt, aber keine der Signaturen spezifischer ist als die anderen, wird ein AmbiguousMethods-Fehler ausgelöst.

Beispielsweise sind die folgenden beiden Implementierungen mehrdeutig, wenn die Funktion foo() jemals mit zwei Ganzzahlargumenten aufgerufen wird, da beide Signaturen gelten würden, aber keine Signatur spezifischer ist als die andere (d. h. keine impliziert die andere):

def foo(bar:int, baz:object):
    pass

@overload
def foo(bar:object, baz:int):
    pass

Im Gegensatz dazu können die folgenden beiden Implementierungen niemals mehrdeutig sein, da eine Signatur immer die andere impliziert; die Signatur int/int ist spezifischer als die Signatur object/object.

def foo(bar:object, baz:object):
    pass

@overload
def foo(bar:int, baz:int):
    pass

Eine Signatur S1 impliziert eine andere Signatur S2, wenn immer wenn S1 gelten würde, auch S2 gelten würde. Eine Signatur S1 ist "spezifischer" als eine andere Signatur S2, wenn S1 S2 impliziert, aber S2 S1 nicht impliziert.

Obwohl die obigen Beispiele alle konkrete oder abstrakte Typen als Argumentannotationen verwendet haben, gibt es keine Anforderung, dass die Annotationen solche sein müssen. Sie können auch "Schnittstellen"-Objekte sein (diskutiert im Abschnitt Schnittstellen und Anpassung), einschließlich benutzerdefinierter Schnittstellentypen. (Sie können auch andere Objekte sein, deren Typen über die Erweiterungs-API ordnungsgemäß registriert sind.)

Weiter zur "nächsten" Methode

Wenn der erste Parameter einer überladenen Funktion __proceed__ genannt wird, wird ihr ein aufrufbares Objekt übergeben, das die nächstspezifischste Methode darstellt. Zum Beispiel dieser Code

def foo(bar:object, baz:object):
    print "got objects!"

@overload
def foo(__proceed__, bar:int, baz:int):
    print "got integers!"
    return __proceed__(bar, baz)

gibt "got integers!" gefolgt von "got objects!" aus.

Wenn es keine nächstspezifischste Methode gibt, wird __proceed__ an eine Instanz von NoApplicableMethods gebunden. Beim Aufruf wird eine neue Instanz von NoApplicableMethods ausgelöst, mit den an die erste Instanz übergebenen Argumenten.

Ebenso, wenn die nächstspezifischeren Methoden bezüglich einander mehrdeutige Präzedenzen haben, wird __proceed__ an eine Instanz von AmbiguousMethods gebunden, und wenn sie aufgerufen wird, wird sie eine neue Instanz auslösen.

Somit kann eine Methode entweder prüfen, ob __proceed__ eine Fehlerinstanz ist, oder sie einfach aufrufen. Die Fehlerklassen NoApplicableMethods und AmbiguousMethods haben eine gemeinsame Basisklasse DispatchError, daher reicht isinstance(__proceed__, overloading.DispatchError) aus, um zu erkennen, ob __proceed__ sicher aufgerufen werden kann.

(Implementierungshinweis: Die Verwendung eines magischen Argumentnamens wie __proceed__ könnte potenziell durch eine magische Funktion ersetzt werden, die aufgerufen würde, um die nächste Methode zu erhalten. Eine magische Funktion würde jedoch die Leistung beeinträchtigen und könnte auf Nicht-CPython-Plattformen schwieriger zu implementieren sein. Methodenverkettung über magische Argumentnamen kann jedoch auf jeder Python-Plattform effizient implementiert werden, die das Erstellen von gebundenen Methoden aus Funktionen unterstützt – man bindet einfach jede zu verkettende Funktion rekursiv, wobei die folgende Funktion oder der Fehler als im_self der gebundenen Methode verwendet wird.)

"Vorher" und "Nachher" Methoden

Zusätzlich zur einfachen Verkettung der nächsten Methode, wie oben gezeigt, ist es manchmal nützlich, andere Wege zur Kombination von Methoden zu haben. Zum Beispiel kann das "Observer-Pattern" manchmal implementiert werden, indem zusätzliche Methoden zu einer Funktion hinzugefügt werden, die vor oder nach der normalen Implementierung ausgeführt werden.

Um diese Anwendungsfälle zu unterstützen, wird das Modul overloading die Dekoratoren @before, @after und @around bereitstellen, die grob den gleichen Methodentypen im Common Lisp Object System (CLOS) oder den entsprechenden "Advice"-Typen in AspectJ entsprechen.

Wie @when müssen alle diese Dekoratoren mit der zu überladenden Funktion übergeben werden und können optional auch ein Prädikat akzeptieren.

from overloading import before, after

def begin_transaction(db):
    print "Beginning the actual transaction"

@before(begin_transaction)
def check_single_access(db: SingletonDB):
    if db.inuse:
        raise TransactionError("Database already in use")

@after(begin_transaction)
def start_logging(db: LoggableDB):
    db.set_log_level(VERBOSE)

@before und @after Methoden werden entweder vor oder nach dem Hauptfunktionskörper aufgerufen und werden *niemals als mehrdeutig betrachtet*. Das heißt, es werden keine Fehler ausgelöst, wenn mehrere "vorherige" oder "nachherige" Methoden mit identischen oder überlappenden Signaturen vorhanden sind. Mehrdeutigkeiten werden durch die Reihenfolge gelöst, in der die Methoden zur Ziel funktion hinzugefügt wurden.

"Vorher"-Methoden werden von der spezifischsten Methode zuerst aufgerufen, wobei mehrdeutige Methoden in der Reihenfolge ausgeführt werden, in der sie zur Ziel funktion hinzugefügt wurden. Alle "vorherigen" Methoden werden aufgerufen, bevor irgendeine der "primären" Methoden der Funktion (d. h. normale @overload-Methoden) ausgeführt wird.

"Nachher"-Methoden werden in *umgekehrter* Reihenfolge aufgerufen, nachdem alle "primären" Methoden der Funktion ausgeführt wurden. Das heißt, sie werden von der am wenigsten spezifischen Methode zuerst ausgeführt, wobei mehrdeutige Methoden in umgekehrter Reihenfolge ihrer Hinzufügung ausgeführt werden.

Die Rückgabewerte von "vorherigen" und "nachherigen" Methoden werden ignoriert, und alle unbehandelten Ausnahmen, die von *irgendeiner* Methode (primär oder anderweitig) ausgelöst werden, beenden sofort den Dispatch-Prozess. "Vorher"- und "Nachher"-Methoden können keine __proceed__-Argumente haben, da sie nicht für den Aufruf anderer Methoden verantwortlich sind. Sie werden einfach als Benachrichtigung vor oder nach den primären Methoden aufgerufen.

Daher können "vorherige" und "nachherige" Methoden verwendet werden, um Vorbedingungen zu überprüfen oder herzustellen (z. B. durch Auslösen eines Fehlers, wenn die Bedingungen nicht erfüllt sind) oder um Nachbedingungen sicherzustellen, ohne vorhandene Funktionalität duplizieren zu müssen.

"Umgebungs"-Methoden

Der Dekorator @around deklariert eine Methode als "around"-Methode. "Around"-Methoden sind weitgehend wie primäre Methoden, mit der Ausnahme, dass die am wenigsten spezifische "around"-Methode eine höhere Präzedenz hat als die spezifischste "before"-Methode.

Im Gegensatz zu "vorherigen" und "nachherigen" Methoden sind "around"-Methoden jedoch dafür verantwortlich, ihr __proceed__-Argument aufzurufen, um den Aufrufprozess fortzusetzen. "Around"-Methoden werden üblicherweise verwendet, um Eingabeargumente oder Rückgabewerte zu transformieren oder um spezifische Fälle mit spezieller Fehlerbehandlung oder Try/Finally-Bedingungen zu umhüllen, z. B.

from overloading import around

@around(commit_transaction)
def lock_while_committing(__proceed__, db: SingletonDB):
    with db.global_lock:
        return __proceed__(db)

Sie können auch verwendet werden, um die normale Behandlung für einen spezifischen Fall zu ersetzen, indem die __proceed__-Funktion *nicht* aufgerufen wird.

Der __proceed__, der einer "around"-Methode übergeben wird, ist entweder die nächste anwendbare "around"-Methode, eine DispatchError-Instanz oder ein synthetisches Methodenobjekt, das alle "vorherigen" Methoden aufruft, gefolgt von der primären Methodenkette, gefolgt von allen "nachherigen" Methoden, und das Ergebnis von der primären Methodenkette zurückgibt.

Somit kann, genau wie bei normalen Methoden, __proceed__ auf DispatchError-Eigenschaften geprüft oder einfach aufgerufen werden. Die "around"-Methode sollte den von __proceed__ zurückgegebenen Wert zurückgeben, es sei denn, sie möchte ihn natürlich modifizieren oder durch einen anderen Rückgabewert für die Funktion als Ganzes ersetzen.

Benutzerdefinierte Kombinationen

Die oben beschriebenen Dekoratoren (@overload, @when, @before, @after und @around) implementieren zusammen, was in CLOS die "Standardmethodenkombination" genannt wird – die gängigsten Muster zur Kombination von Methoden.

Manchmal hat jedoch eine Anwendung oder Bibliothek Verwendung für einen ausgefeilteren Typ der Methodenkombination. Wenn Sie beispielsweise "Rabatt"-Methoden haben möchten, die einen Prozentsatz Rabatt zurückgeben, der vom Wert der primären Methode(n) abgezogen werden soll, könnten Sie etwa so etwas schreiben:

from overloading import always_overrides, merge_by_default
from overloading import Around, Before, After, Method, MethodList

class Discount(MethodList):
    """Apply return values as discounts"""

    def __call__(self, *args, **kw):
        retval = self.tail(*args, **kw)
        for sig, body in self.sorted():
            retval -= retval * body(*args, **kw)
        return retval

# merge discounts by priority
merge_by_default(Discount)

# discounts have precedence over before/after/primary methods
always_overrides(Discount, Before)
always_overrides(Discount, After)
always_overrides(Discount, Method)

# but not over "around" methods
always_overrides(Around, Discount)

# Make a decorator called "discount" that works just like the
# standard decorators...
discount = Discount.make_decorator('discount')

# and now let's use it...
def price(product):
    return product.list_price

@discount(price)
def ten_percent_off_shoes(product: Shoe)
    return Decimal('0.1')

Ähnliche Techniken können verwendet werden, um eine Vielzahl von CLOS-Style-Methodenqualifizierern und Kombinationsregeln zu implementieren. Der Prozess des Erstellens benutzerdefinierter Methodenkombinationsobjekte und ihrer entsprechenden Dekoratoren wird im Abschnitt Erweiterungs-API detaillierter beschrieben.

Übrigens, der gezeigte @discount-Dekorator funktioniert korrekt mit allen neuen Prädikaten, die von anderem Code definiert wurden. Wenn beispielsweise zope.interface seine Schnittstellentypen registrieren würde, damit sie korrekt als Argumentannotationen funktionieren, könnten Sie Rabatte basierend auf seinen Schnittstellentypen angeben, nicht nur auf Klassen oder von overloading definierten Schnittstellentypen.

Ebenso, wenn eine Bibliothek wie RuleDispatch oder PEAK-Rules eine geeignete Prädikatinplementierung und ein Dispatching-Engine registrieren würde, könnte man diese Prädikate dann auch für Rabatte verwenden, z. B.

from somewhere import Pred  # some predicate implementation

@discount(
    price,
    Pred("isinstance(product,Shoe) and"
         " product.material.name=='Blue Suede'")
)
def forty_off_blue_suede_shoes(product):
    return Decimal('0.4')

Der Prozess der Definition benutzerdefinierter Prädikattypen und Dispatching-Engines wird ebenfalls im Abschnitt Erweiterungs-API detaillierter beschrieben.

Überladung innerhalb von Klassen

Alle oben genannten Dekoratoren haben ein spezielles zusätzliches Verhalten, wenn sie direkt innerhalb eines Klassenkörpers aufgerufen werden: Der erste Parameter (außer __proceed__, falls vorhanden) der dekorierten Funktion wird so behandelt, als hätte er eine Annotation, die gleich der Klasse ist, in der sie definiert wurde.

Das heißt, dieser Code

class And(object):
    # ...
    @when(get_conjuncts)
    def __conjuncts(self):
        return self.conjuncts

erzeugt denselben Effekt wie dieser (abgesehen von der Existenz einer privaten Methode)

class And(object):
    # ...

@when(get_conjuncts)
def get_conjuncts_of_and(ob: And):
    return ob.conjuncts

Dieses Verhalten ist sowohl eine Komfortverbesserung bei der Definition vieler Methoden als auch eine Anforderung für die sichere Unterscheidung von Mehrfachargument-Überladungen in Unterklassen. Betrachten Sie beispielsweise den folgenden Code

class A(object):
    def foo(self, ob):
        print "got an object"

    @overload
    def foo(__proceed__, self, ob:Iterable):
        print "it's iterable!"
        return __proceed__(self, ob)


class B(A):
    foo = A.foo     # foo must be defined in local namespace

    @overload
    def foo(__proceed__, self, ob:Iterable):
        print "B got an iterable!"
        return __proceed__(self, ob)

Aufgrund der impliziten Klassenregel gibt der Aufruf von B().foo([]) "B got an iterable!" gefolgt von "it's iterable!", und schließlich "got an object" aus, während A().foo([]) nur die in A definierten Nachrichten ausgeben würde.

Umgekehrt, ohne die implizite Klassenregel, hätten die beiden "Iterable"-Methoden exakt dieselben Anwendbarkeitsbedingungen, sodass der Aufruf von entweder A().foo([]) oder B().foo([]) zu einem Fehler AmbiguousMethods führen würde.

Es ist derzeit eine offene Frage, wie diese Regel in Python 3.0 am besten umgesetzt werden kann. Unter Python 2.x wurde die Metaklasse einer Klasse erst am Ende des Klassenkörpers gewählt, was bedeutet, dass Dekoratoren eine benutzerdefinierte Metaklasse einfügen konnten, um solche Verarbeitungen durchzuführen. (So implementiert z. B. RuleDispatch die implizite Klassenregel.)

PEP 3115 erfordert jedoch, dass die Metaklasse einer Klasse *vor* der Ausführung des Klassenkörpers bestimmt wird, was es unmöglich macht, diese Technik mehr für Klassendekorationen zu verwenden.

Zum Zeitpunkt der Erstellung dieser Zeilen sind die Diskussionen zu diesem Thema noch im Gange.

Schnittstellen und Anpassung

Das Modul overloading bietet eine einfache Implementierung von Schnittstellen und Anpassung. Das folgende Beispiel definiert eine IStack-Schnittstelle und erklärt, dass list-Objekte diese unterstützen:

from overloading import abstract, Interface

class IStack(Interface):
    @abstract
    def push(self, ob)
        """Push 'ob' onto the stack"""

    @abstract
    def pop(self):
        """Pop a value and return it"""


when(IStack.push, (list, object))(list.append)
when(IStack.pop, (list,))(list.pop)

mylist = []
mystack = IStack(mylist)
mystack.push(42)
assert mystack.pop()==42

Die Klasse Interface ist eine Art "universeller Adapter". Sie akzeptiert ein einzelnes Argument: ein Objekt zum Anpassen. Sie bindet dann alle ihre Methoden an das Zielobjekt, anstelle von sich selbst. Somit ist der Aufruf von mystack.push(42) dasselbe wie der Aufruf von IStack.push(mylist, 42).

Der Dekorator @abstract markiert eine Funktion als abstrakt: d. h. sie hat keine Implementierung. Wenn eine @abstract-Funktion aufgerufen wird, löst sie NoApplicableMethods aus. Um ausführbar zu werden, müssen überladene Methoden mit den zuvor beschriebenen Techniken hinzugefügt werden. (Das heißt, Methoden können mit @when, @before, @after, @around oder beliebigen benutzerdefinierten Methodenkombinations-Dekoratoren hinzugefügt werden.)

Im obigen Beispiel wird die Methode list.append als Methode für IStack.push() hinzugefügt, wenn ihre Argumente eine Liste und ein beliebiges Objekt sind. Somit wird IStack.push(mylist, 42) zu list.append(mylist, 42) übersetzt, wodurch die gewünschte Operation implementiert wird.

Abstrakte und konkrete Methoden

Beachten Sie übrigens, dass der Dekorator @abstract nicht nur zur Definition von Schnittstellen verwendet werden kann; er kann überall dort eingesetzt werden, wo Sie eine "leere" generische Funktion erstellen möchten, die zunächst keine Methoden hat. Insbesondere muss er nicht innerhalb einer Klasse verwendet werden.

Beachten Sie auch, dass Schnittstellenmethoden nicht abstrakt sein müssen; man könnte beispielsweise eine Schnittstelle wie folgt schreiben:

class IWriteMapping(Interface):
    @abstract
    def __setitem__(self, key, value):
        """This has to be implemented"""

    def update(self, other:IReadMapping):
        for k, v in IReadMapping(other).items():
            self[k] = v

Solange __setitem__ für einen bestimmten Typ definiert ist, bietet die obige Schnittstelle eine nutzbare update()-Implementierung. Wenn jedoch ein bestimmter Typ (oder ein Paar von Typen) eine effizientere Methode zur Verarbeitung von update()-Operationen hat, kann dennoch eine entsprechende Überladung für diesen Fall registriert werden.

Unterklassenbildung und Wiederzusammenfügung

Schnittstellen können von der Unterklasse gebildet werden

class ISizedStack(IStack):
    @abstract
    def __len__(self):
        """Return the number of items on the stack"""

# define __len__ support for ISizedStack
when(ISizedStack.__len__, (list,))(list.__len__)

Oder durch Kombination von Funktionen aus bestehenden Schnittstellen

class Sizable(Interface):
    __len__ = ISizedStack.__len__

# list now implements Sizable as well as ISizedStack, without
# making any new declarations!

Eine Klasse kann zu einem bestimmten Zeitpunkt als "Anpassung" an eine Schnittstelle betrachtet werden, wenn keine in der Schnittstelle definierte Methode garantiert einen NoApplicableMethods-Fehler auslöst, wenn sie auf eine Instanz dieser Klasse zu diesem Zeitpunkt angewendet wird.

Im normalen Gebrauch ist es jedoch "einfacher um Verzeihung zu bitten als um Erlaubnis". Das heißt, es ist einfacher, eine Schnittstelle auf einem Objekt zu verwenden, indem man es an die Schnittstelle anpasst (z. B. IStack(mylist)) oder Schnittstellenmethoden direkt aufruft (z. B. IStack.push(mylist, 42)), als zu versuchen herauszufinden, ob das Objekt an die Schnittstelle angepasst werden kann (oder sie direkt implementiert).

Implementierung einer Schnittstelle in einer Klasse

Es ist möglich zu deklarieren, dass eine Klasse eine Schnittstelle direkt implementiert, mithilfe der Funktion declare_implementation().

from overloading import declare_implementation

class Stack(object):
    def __init__(self):
        self.data = []
    def push(self, ob):
        self.data.append(ob)
    def pop(self):
        return self.data.pop()

declare_implementation(IStack, Stack)

Der obige Aufruf von declare_implementation() ist ungefähr gleichwertig mit folgenden Schritten:

when(IStack.push, (Stack,object))(lambda self, ob: self.push(ob))
when(IStack.pop, (Stack,))(lambda self, ob: self.pop())

Das heißt, der Aufruf von IStack.push() oder IStack.pop() auf einer Instanz einer beliebigen Unterklasse von Stack delegiert einfach an die tatsächlichen push()- oder pop()-Methoden davon.

Aus Effizienzgründen *kann* der Aufruf von IStack(s), wobei s eine Instanz von Stack ist, s zurückgeben und nicht einen IStack-Adapter. (Beachten Sie, dass der Aufruf von IStack(x), wobei x bereits ein IStack-Adapter ist, immer unverändert x zurückgibt; dies ist eine zusätzliche Optimierung, die in Fällen erlaubt ist, in denen das Adaptee bekanntermaßen die Schnittstelle *direkt* implementiert, ohne Anpassung.)

Der Bequemlichkeit halber kann es nützlich sein, Implementierungen im Klassenheader zu deklarieren, z. B.:

class Stack(metaclass=Implementer, implements=IStack):
    ...

Anstatt declare_implementation() nach dem Ende des Suites aufzurufen.

Schnittstellen als Typenspezifizierer

Interface-Unterklassen können als Argumentannotationen verwendet werden, um anzugeben, welche Art von Objekten von einer Überladung akzeptiert werden, z. B.:

@overload
def traverse(g: IGraph, s: IStack):
    g = IGraph(g)
    s = IStack(s)
    # etc....

Beachten Sie jedoch, dass die tatsächlichen Argumente durch die bloße Verwendung einer Schnittstelle als Typenspezifizierer *nicht* verändert oder angepasst werden. Sie müssen die Objekte explizit in die entsprechende Schnittstelle umwandeln, wie oben gezeigt.

Beachten Sie jedoch, dass andere Muster der Schnittstellenverwendung möglich sind. Andere Schnittstellenimplementierungen unterstützen möglicherweise keine Anpassung oder erfordern, dass Funktionsargumente bereits an die angegebene Schnittstelle angepasst sind. Die genauen Semantiken der Verwendung einer Schnittstelle als Typenspezifizierer hängen daher von den tatsächlich verwendeten Schnittstellenobjekten ab.

Für die von diesem PEP bereitgestellten Schnittstellenobjekte sind die Semantiken jedoch wie oben beschrieben. Eine Schnittstelle I1 gilt als "spezifischer" als eine andere Schnittstelle I2, wenn die Menge der Deskriptoren in der Vererbungshierarchie von I1 eine echte Obermenge der Deskriptoren in der Vererbungshierarchie von I2 ist.

Zum Beispiel ist ISizedStack spezifischer als sowohl ISizable als auch ISizedStack, unabhängig von den Vererbungsbeziehungen zwischen diesen Schnittstellen. Es ist reine Frage, welche Operationen innerhalb dieser Schnittstellen enthalten sind – und die *Namen* der Operationen sind unwichtig.

Schnittstellen (zumindest die von overloading bereitgestellten) gelten immer als weniger spezifisch als konkrete Klassen. Andere Schnittstellenimplementierungen können ihre eigenen Spezifitätsregeln festlegen, sowohl zwischen Schnittstellen und anderen Schnittstellen als auch zwischen Schnittstellen und Klassen.

Nicht-Methodenattribute in Schnittstellen

Die Interface-Implementierung behandelt tatsächlich alle Attribute und Methoden (d. h. Deskriptoren) auf die gleiche Weise: ihre __get__- (und __set__ und __delete__-, falls vorhanden) Methoden werden mit dem umschlossenen (angepassten) Objekt als "self" aufgerufen. Für Funktionen hat dies den Effekt, dass eine gebundene Methode erstellt wird, die die generische Funktion mit dem umschlossenen Objekt verknüpft.

Für Nicht-Funktionsattribute kann es am einfachsten sein, sie mit dem property-Built-in und den entsprechenden fget-, fset- und fdel-Attributen zu spezifizieren:

class ILength(Interface):
    @property
    @abstract
    def length(self):
        """Read-only length attribute"""

# ILength(aList).length == list.__len__(aList)
when(ILength.length.fget, (list,))(list.__len__)

Alternativ können Methoden wie _get_foo() und _set_foo() als Teil der Schnittstelle definiert und die Eigenschaft basierend auf diesen Methoden definiert werden, aber dies ist für Benutzer schwieriger korrekt zu implementieren, wenn sie eine Klasse erstellen, die die Schnittstelle direkt implementiert, da sie dann alle individuellen Methodennamen abgleichen müssten, nicht nur den Namen der Eigenschaft oder des Attributs.

Aspekte

Das oben beschriebene Anpassungssystem geht davon aus, dass Adapter "zustandslos" sind, d. h. Adapter haben keine Attribute oder Zustände außer denen des angepassten Objekts. Dies folgt dem "Typeclass/Instance"-Modell von Haskell und dem Konzept von "reinen" (d. h. transitiv komponierbaren) Adaptern.

Es gibt jedoch gelegentlich Fälle, in denen zur Bereitstellung einer vollständigen Implementierung einer Schnittstelle eine zusätzliche Art von Zustand erforderlich ist.

Eine Möglichkeit wäre natürlich, "private" Attribute an das Adaptee zu hängen (Monkey-Patching). Aber das ist anfällig für Namenskollisionen und erschwert den Initialisierungsprozess (da jeder Code, der diese Attribute verwendet, deren Existenz prüfen und sie bei Bedarf initialisieren muss). Es funktioniert auch nicht bei Objekten, die kein __dict__-Attribut haben.

Daher wird die Klasse Aspect bereitgestellt, um das einfache Anhängen zusätzlicher Informationen an Objekte zu ermöglichen, die entweder:

  1. ein __dict__-Attribut haben (sodass Instanzen von Aspekten darin gespeichert werden können, indiziert nach Aspektklasse),
  2. schwache Referenzierung unterstützen (sodass Instanzen von Aspekten über ein globales, aber threadsicheres Schwachreferenzwörterbuch verwaltet werden können), oder
  3. die Schnittstelle overloading.IAspectOwner implementieren oder sich daran anpassen können (technisch gesehen implizieren #1 oder #2 dies).

Die Unterklassenbildung von Aspect erstellt eine Adapterklasse, deren Zustand an die Lebensdauer des angepassten Objekts gebunden ist.

Angenommen, Sie möchten beispielsweise zählen, wie oft eine bestimmte Methode auf Instanzen von Target aufgerufen wird (ein klassisches AOP-Beispiel). Sie könnten etwas Ähnliches tun:

from overloading import Aspect

class Count(Aspect):
    count = 0

@after(Target.some_method)
def count_after_call(self:Target, *args, **kw):
    Count(self).count += 1

Der obige Code zählt die Anzahl der erfolgreichen Aufrufe von Target.some_method() auf einer Instanz von Target (d. h. er zählt keine Fehler, es sei denn, sie treten in einer spezifischeren "after"-Methode auf). Andere Codes können dann über Count(someTarget).count auf die Anzahl zugreifen.

Aspect-Instanzen können natürlich __init__-Methoden haben, um beliebige Datenstrukturen zu initialisieren. Sie können entweder __slots__ oder dictionary-basierte Attribute für die Speicherung verwenden.

Obwohl diese Einrichtung im Vergleich zu einem voll ausgestatteten AOP-Werkzeug wie AspectJ eher primitiv ist, können Personen, die Pointcut-Bibliotheken oder andere AspectJ-ähnliche Features erstellen möchten, sicherlich Aspect-Objekte und Methodenkombinations-Dekoratoren als Basis für die Erstellung ausdrucksstärkerer AOP-Tools verwenden.

XXX Spezifikation der vollständigen Aspect-API, einschließlich Schlüssel, N-zu-1-Aspekte, manuelles
Anbringen/Ablösen/Löschen von Aspektinstanzen und der IAspectOwner-Schnittstelle.

Erweiterungs-API

TODO: erklären, wie all diese funktionieren

impliziert(o1, o2)

deklariere_implementierung(iface, klasse)

prädikatsignaturen(ob)

parse_regel(ruleset, body, prädikat, actiontype, localdict, globaldict)

kombiniere_aktionen(a1, a2)

regeln_für(f)

Rule-Objekte

ActionDef-Objekte

RuleSet-Objekte

Methoden-Objekte

Methodenlisten-Objekte

IAspectOwner

Nutzungsmuster der Überladung

In einer Diskussion auf der Python-3000-Liste war das vorgeschlagene Feature, beliebige Funktionen überladen zu lassen, etwas umstritten, wobei einige Leute Bedenken äußerten, dass dies Programme schwerer verständlich machen würde.

Die allgemeine Stoßrichtung dieses Arguments ist, dass man sich nicht darauf verlassen kann, was eine Funktion tut, wenn sie jederzeit von überall im Programm geändert werden kann. Auch wenn dies prinzipiell bereits durch Monkeypatching oder Code-Substitution geschehen kann, wird dies als schlechte Praxis angesehen.

Die Unterstützung für das Überladen jeder Funktion (so das Argument) würde jedoch implizit solche Änderungen als akzeptable Praxis segnen.

Dieses Argument erscheint theoretisch sinnvoll, ist aber in der Praxis aus zwei Gründen fast vollständig gegenstandslos.

Erstens sind die Leute im Allgemeinen nicht pervers und definieren eine Funktion an einer Stelle so, dass sie etwas tut, und definieren sie dann an anderer Stelle summarisch so um, dass sie das Gegenteil tut! Die Hauptgründe, das Verhalten einer Funktion zu erweitern, die *nicht* speziell generisch gemacht wurde, sind:

  • Hinzufügen von Sonderfällen, die vom Autor der ursprünglichen Funktion nicht bedacht wurden, wie z. B. Unterstützung für zusätzliche Typen.
  • Benachrichtigt werden über eine Aktion, um eine damit zusammenhängende Operation auszulösen, entweder vor der ursprünglichen Operation, nach ihr oder beides. Dies kann allgemeine Operationen wie das Hinzufügen von Protokollierung, Zeitmessung oder Nachverfolgung sowie anwendungsspezifisches Verhalten umfassen.

Keiner dieser Gründe für das Hinzufügen von Überladungen impliziert eine Änderung des beabsichtigten Standard- oder Gesamtverhaltens der vorhandenen Funktion. So wie eine Basisklassenmethode von einer Unterklasse aus denselben beiden Gründen überschrieben werden kann, so kann auch eine Funktion überladen werden, um solche Erweiterungen zu ermöglichen.

Mit anderen Worten, universelles Überladen ist nicht gleichbedeutend mit *beliebigem* Überladen, in dem Sinne, dass wir nicht erwarten müssen, dass Leute das Verhalten bestehender Funktionen auf unlogische oder unvorhersehbare Weise neu definieren. Wenn sie dies täten, wäre es keine schlechtere Praxis als jede andere Art, unlogischen oder unvorhersehbaren Code zu schreiben!

Um jedoch schlechte von guter Praxis zu unterscheiden, ist es vielleicht notwendig, genauer zu klären, was gute Praxis für die Definition von Überladungen *ist*. Und das bringt uns zum zweiten Grund, warum generische Funktionen Programme nicht unbedingt schwerer verständlich machen: Überladungen in tatsächlichen Programmen folgen tendenziell sehr vorhersagbaren Mustern. (Sowohl in Python als auch in Sprachen, die keine *nicht*-generischen Funktionen haben.)

Wenn ein Modul eine neue generische Operation definiert, wird es normalerweise auch alle erforderlichen Überladungen für bestehende Typen an derselben Stelle definieren. Ebenso, wenn ein Modul einen neuen Typ definiert, wird es dort normalerweise Überladungen für alle generischen Funktionen definieren, die es kennt oder um die es sich kümmert.

Infolgedessen sind die überwiegende Mehrheit der Überladungen entweder neben der überladenen Funktion oder neben einem neu definierten Typ zu finden, für den die Überladung Unterstützung hinzufügt. Somit sind Überladungen im häufigen Fall sehr gut auffindbar, da man sich entweder die Funktion oder den Typ ansieht, oder beides.

Nur in eher seltenen Fällen findet man Überladungen in einem Modul, das weder die Funktion noch den Typ (bzw. die Typen) enthält, für die die Überladung hinzugefügt wird. Dies wäre der Fall, wenn beispielsweise ein Dritter eine Brücke der Unterstützung zwischen den Typen einer Bibliothek und den generischen Funktionen einer anderen Bibliothek schafft. In einem solchen Fall schlägt die beste Praxis jedoch vor, dies deutlich zu bewerben, insbesondere durch den Modulnamen.

Zum Beispiel definiert PyProtocols solche Brückenunterstützung für die Arbeit mit Zope-Schnittstellen und Legacy-Twisted-Schnittstellen unter Verwendung von Modulen namens protocols.twisted_support und protocols.zope_support. (Diese Brücken werden mit Schnittstellenadaptern anstelle von generischen Funktionen realisiert, aber das Grundprinzip ist dasselbe.)

Kurz gesagt, das Verständnis von Programmen im Hinblick auf universelles Überladen muss nicht schwieriger sein, da die überwiegende Mehrheit der Überladungen entweder neben einer Funktion oder neben der Definition eines Typs liegt, der an diese Funktion übergeben wird.

Und mangels Inkompetenz oder bewusster Absicht, unklar zu sein, müssen die wenigen Überladungen, die nicht neben den relevanten Typ(en) oder Funktion(en) liegen, im Allgemeinen nicht außerhalb des Geltungsbereichs verstanden oder bekannt sein, in dem diese Überladungen definiert sind. (Ausgenommen ist der Fall der „Support-Module“, wo die beste Praxis vorschlägt, diese entsprechend zu benennen.)

Implementierungs-Hinweise

Der Großteil der in diesem PEP beschriebenen Funktionalität ist bereits in der sich in Entwicklung befindlichen Version des PEAK-Rules-Frameworks implementiert. Insbesondere das grundlegende Überladungs- und Methoden-Kombinations-Framework (ohne den `@overload`-Decorator) existiert dort bereits. Die Implementierung all dieser Funktionen in peak.rules.core umfasst zum Zeitpunkt der Erstellung dieses Dokuments 656 Zeilen Python.

peak.rules.core stützt sich derzeit auf die Module DecoratorTools und BytecodeAssembler, aber beide Abhängigkeiten können ersetzt werden, da DecoratorTools hauptsächlich für die Python 2.3-Kompatibilität und zur Implementierung von Strukturtypen verwendet wird (was in späteren Python-Versionen mit benannten Tupeln erfolgen kann). Die Verwendung von BytecodeAssembler kann mit einem „exec“- oder „compile“-Workaround ersetzt werden, wenn einigermaßen Aufwand betrieben wird. (Es wäre einfacher, dies zu tun, wenn das Attribut `func_closure` von Funktions-Objekten beschreibbar wäre.)

Die Interface-Klasse wurde bereits früher als Prototyp entwickelt, ist aber derzeit nicht in PEAK-Rules enthalten.

Die „implizite Klassenregel“ wurde bereits in der RuleDispatch-Bibliothek implementiert. Sie stützt sich jedoch auf den `__metaclass__`-Hook, der in PEP 3115 derzeit eliminiert wird.

Ich weiß derzeit nicht, wie ich `@overload` in Klassen-Körpern mit `classmethod` und `staticmethod` gut zusammenspielen lassen kann. Es ist jedoch nicht wirklich klar, ob dies notwendig ist.


Quelle: https://github.com/python/peps/blob/main/peps/pep-3124.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT