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

Python Enhancement Proposals

PEP 3119 – Einführung von Abstrakten Basisklassen

Autor:
Guido van Rossum <guido at python.org>, Talin <viridia at gmail.com>
Status:
Final
Typ:
Standards Track
Erstellt:
18. Apr. 2007
Python-Version:
3.0
Post-History:
26. Apr. 2007, 11. Mai 2007

Inhaltsverzeichnis

Zusammenfassung

Dies ist ein Vorschlag zur Hinzufügung von Unterstützung für Abstrakte Basisklassen (ABCs) zu Python 3000. Es wird vorgeschlagen:

  • Eine Möglichkeit, isinstance() und issubclass() zu überladen.
  • Ein neues Modul abc, das als „ABC-Unterstützungsframework“ dient. Es definiert eine Metaklasse für die Verwendung mit ABCs und einen Dekorator, der zur Definition abstrakter Methoden verwendet werden kann.
  • Spezifische ABCs für Container und Iteratoren, die dem `collections`-Modul hinzugefügt werden sollen.

Ein Großteil des Denkens, das in den Vorschlag eingeflossen ist, betrifft nicht den spezifischen Mechanismus von ABCs im Gegensatz zu Schnittstellen oder generischen Funktionen (GFs), sondern die Klärung philosophischer Fragen wie „Was macht eine Menge aus?“, „Was macht ein Mapping aus?“ und „Was macht eine Sequenz aus?“.

Es gibt auch einen begleitenden PEP 3141, der ABCs für numerische Typen definiert.

Danksagungen

Talin schrieb die unten stehende Begründung [1] sowie den größten Teil des Abschnitts über ABCs vs. Schnittstellen. Allein dafür verdient er eine Co-Autorschaft. Der Rest des PEP verwendet „Ich“ in Bezug auf den Erstautor.

Begründung

Im Bereich der objektorientierten Programmierung lassen sich die Muster der Interaktion mit einem Objekt in zwei grundlegende Kategorien einteilen: „Invocation“ und „Inspection“.

Invocation bedeutet die Interaktion mit einem Objekt durch Aufruf seiner Methoden. Normalerweise wird dies mit Polymorphie kombiniert, sodass der Aufruf einer bestimmten Methode je nach Typ eines Objekts unterschiedlichen Code ausführen kann.

Inspection bedeutet die Fähigkeit für externen Code (außerhalb der Methoden des Objekts), den Typ oder die Eigenschaften dieses Objekts zu untersuchen und basierend auf diesen Informationen zu entscheiden, wie dieses Objekt behandelt werden soll.

Beide Nutzungsmuster dienen demselben allgemeinen Zweck, nämlich der Fähigkeit, die Verarbeitung vielfältiger und potenziell neuartiger Objekte auf einheitliche Weise zu unterstützen und gleichzeitig Entscheidungen über die Verarbeitung für jeden verschiedenen Objekttyp anzupassen.

In der klassischen OOP-Theorie ist Invocation das bevorzugte Nutzungsmuster, und Inspection wird aktiv entmutigt und als Relikt eines früheren, prozeduralen Programmierstils betrachtet. In der Praxis ist diese Ansicht jedoch einfach zu dogmatisch und unflexibel und führt zu einer Art Design-Rigidität, die im Widerspruch zur dynamischen Natur einer Sprache wie Python steht.

Insbesondere besteht oft die Notwendigkeit, Objekte auf eine Weise zu verarbeiten, die vom Ersteller der Objektklasse nicht vorhergesehen wurde. Es ist nicht immer die beste Lösung, in jedes Objekt Methoden einzubauen, die den Bedürfnissen jedes möglichen Benutzers dieses Objekts entsprechen. Darüber hinaus gibt es viele mächtige Dispatch-Philosophien, die im direkten Gegensatz zur klassischen OOP-Anforderung stehen, dass Verhalten streng innerhalb eines Objekts gekapselt ist, wie z. B. regel- oder mustergesteuerte Logik.

Einer der Kritikpunkte an Inspection durch klassische OOP-Theoretiker ist andererseits der Mangel an Formalismen und die Ad-hoc-Natur dessen, was inspiziert wird. In einer Sprache wie Python, in der fast jeder Aspekt eines Objekts reflektiert und direkt von externem Code aufgerufen werden kann, gibt es viele verschiedene Möglichkeiten, zu testen, ob ein Objekt ein bestimmtes Protokoll erfüllt oder nicht. Wenn man zum Beispiel fragt: „Ist dieses Objekt ein veränderlicher Sequenzcontainer?“, kann man nach einer Basisklasse „list“ suchen oder nach einer Methode namens „__getitem__“. Beachten Sie jedoch, dass diese Tests zwar offensichtlich erscheinen mögen, aber keiner von ihnen korrekt ist, da einer zu falschen Negativen und der andere zu falschen Positiven führt.

Das allgemein anerkannte Mittel ist, die Tests zu standardisieren und in einer formalen Anordnung zu gruppieren. Dies geschieht am einfachsten, indem jeder Klasse eine Reihe von standardmäßigen testbaren Eigenschaften zugeordnet wird, entweder über den Vererbungsmechanismus oder auf andere Weise. Jeder Test bringt eine Reihe von Versprechen mit sich: Er enthält ein Versprechen über das allgemeine Verhalten der Klasse und ein Versprechen darüber, welche anderen Klassenmethoden verfügbar sein werden.

Dieser PEP schlägt eine bestimmte Strategie zur Organisation dieser Tests vor, die als Abstrakte Basisklassen oder ABCs bekannt ist. ABCs sind einfach Python-Klassen, die in den Vererbungsbaum eines Objekts eingefügt werden, um bestimmte Merkmale dieses Objekts für einen externen Inspektor zu signalisieren. Tests werden mit isinstance() durchgeführt, und die Anwesenheit einer bestimmten ABC bedeutet, dass der Test bestanden wurde.

Zusätzlich definieren die ABCs eine minimale Menge von Methoden, die das charakteristische Verhalten des Typs festlegen. Code, der Objekte anhand ihres ABC-Typs unterscheidet, kann darauf vertrauen, dass diese Methoden immer vorhanden sind. Jede dieser Methoden wird von einer verallgemeinerten abstrakten semantischen Definition begleitet, die in der Dokumentation der ABC beschrieben ist. Diese Standard-Semantik-Definitionen werden nicht erzwungen, aber dringend empfohlen.

Wie alle anderen Dinge in Python sind diese Versprechen eher eine freundliche Vereinbarung, was bedeutet, dass die Sprache zwar einige der in der ABC gemachten Versprechen erzwingt, es aber dem Implementierer der konkreten Klasse obliegt, sicherzustellen, dass die restlichen eingehalten werden.

Spezifikation

Die Spezifikation folgt den im Abstract aufgeführten Kategorien

  • Eine Möglichkeit, isinstance() und issubclass() zu überladen.
  • Ein neues Modul abc, das als „ABC-Unterstützungsframework“ dient. Es definiert eine Metaklasse für die Verwendung mit ABCs und einen Dekorator, der zur Definition abstrakter Methoden verwendet werden kann.
  • Spezifische ABCs für Container und Iteratoren, die dem `collections`-Modul hinzugefügt werden sollen.

Überladen von isinstance() und issubclass()

Während der Entwicklung dieses PEP und seines Begleitdokuments PEP 3141 standen wir wiederholt vor der Wahl, entweder mehr, feingranulare ABCs oder weniger, grob granulare ABCs zu standardisieren. Zum Beispiel führte PEP 3141 zu einem Zeitpunkt die folgende Hierarchie von Basisklassen für komplexe Zahlen ein: MonoidUnderPlus, AdditiveGroup, Ring, Field, Complex (jeweils abgeleitet von der vorherigen). Und die Diskussion erwähnte mehrere andere algebraische Kategorisierungen, die weggelassen wurden: Algebraic, Transcendental und IntegralDomain, und PrincipalIdealDomain. In früheren Versionen des aktuellen PEP haben wir die Anwendungsfälle für separate Klassen wie Set, ComposableSet, MutableSet, HashableSet, MutableComposableSet, HashableComposableSet berücksichtigt.

Das Dilemma hier ist, dass wir lieber weniger ABCs hätten, aber was soll ein Benutzer tun, der eine weniger verfeinerte ABC benötigt? Betrachten Sie z. B. das Los eines Mathematikers, der seine eigene Art von Transzendentalzahlen definieren möchte, aber auch möchte, dass float und int als Transzendental betrachtet werden. PEP 3141 schlug ursprünglich vor, `float.__bases__` für diesen Zweck zu patchen, aber es gibt einige gute Gründe, die eingebauten Typen unveränderlich zu halten (zum einen werden sie von allen Python-Interpretern im selben Adressraum gemeinsam genutzt, wie sie von mod_python verwendet werden [16]).

Ein weiteres Beispiel wäre jemand, der eine generische Funktion (PEP 3124) für jede Sequenz definieren möchte, die eine `append()`-Methode hat. Die `Sequence`-ABC (siehe unten) garantiert die `append()`-Methode nicht, während `MutableSequence` nicht nur `append()`, sondern auch verschiedene andere mutierende Methoden erfordert.

Um diese und ähnliche Dilemmata zu lösen, wird im nächsten Abschnitt eine Metaklasse für die Verwendung mit ABCs vorgeschlagen, die es uns ermöglicht, eine ABC als „virtuelle Basisklasse“ (nicht dasselbe Konzept wie in C++) zu jeder Klasse hinzuzufügen, auch zu einer anderen ABC. Dies ermöglicht es der Standardbibliothek, die ABCs `Sequence` und `MutableSequence` zu definieren und diese als virtuelle Basisklassen für integrierte Typen wie `basestring`, `tuple` und `list` zu registrieren, sodass zum Beispiel die folgenden Bedingungen alle wahr sind

isinstance([], Sequence)
issubclass(list, Sequence)
issubclass(list, MutableSequence)
isinstance((), Sequence)
not issubclass(tuple, MutableSequence)
isinstance("", Sequence)
issubclass(bytearray, MutableSequence)

Der primär vorgeschlagene Mechanismus ist, die integrierten Funktionen isinstance() und issubclass() überladen zu können. Das Überladen funktioniert wie folgt: Der Aufruf isinstance(x, C) prüft zuerst, ob C.__instancecheck__ existiert, und wenn ja, ruft C.__instancecheck__(x) anstelle seiner normalen Implementierung auf. Ebenso prüft der Aufruf issubclass(D, C) zuerst, ob C.__subclasscheck__ existiert, und wenn ja, ruft C.__subclasscheck__(D) anstelle seiner normalen Implementierung auf.

Beachten Sie, dass die magischen Namen nicht __isinstance__ und __issubclass__ sind; dies liegt daran, dass die Umkehrung der Argumente zu Verwirrung führen könnte, insbesondere für den issubclass()-Überlader.

Eine prototypische Implementierung davon ist in [12] gegeben.

Hier ist ein Beispiel mit (naiverweise einfachen) Implementierungen von __instancecheck__ und __subclasscheck__

class ABCMeta(type):

    def __instancecheck__(cls, inst):
        """Implement isinstance(inst, cls)."""
        return any(cls.__subclasscheck__(c)
                   for c in {type(inst), inst.__class__})

    def __subclasscheck__(cls, sub):
        """Implement issubclass(sub, cls)."""
        candidates = cls.__dict__.get("__subclass__", set()) | {cls}
        return any(c in candidates for c in sub.mro())

class Sequence(metaclass=ABCMeta):
    __subclass__ = {list, tuple}

assert issubclass(list, Sequence)
assert issubclass(tuple, Sequence)

class AppendableSequence(Sequence):
    __subclass__ = {list}

assert issubclass(list, AppendableSequence)
assert isinstance([], AppendableSequence)

assert not issubclass(tuple, AppendableSequence)
assert not isinstance((), AppendableSequence)

Der nächste Abschnitt schlägt eine vollwertige Implementierung vor.

Das Modul abc: Ein ABC-Unterstützungsframework

Das neue Standardbibliotheksmodul abc, geschrieben in reinem Python, dient als Framework zur Unterstützung von ABCs. Es definiert eine Metaklasse ABCMeta und die Dekoratoren @abstractmethod und @abstractproperty. Eine Beispielimplementierung wird von [13] bereitgestellt.

Die Klasse ABCMeta überschreibt __instancecheck__ und __subclasscheck__ und definiert eine Methode register. Die Methode register nimmt ein Argument entgegen, das eine Klasse sein muss; nach dem Aufruf B.register(C) gibt der Aufruf issubclass(C, B) True zurück, da B.__subclasscheck__(C) True zurückgibt. Ebenso ist isinstance(x, B) äquivalent zu issubclass(x.__class__, B) or issubclass(type(x), B). (Es ist möglich, dass type(x) und x.__class__ nicht dasselbe Objekt sind, z. B. wenn x ein Proxy-Objekt ist.)

Diese Methoden sind für Aufrufe auf Klassen bestimmt, deren Metaklasse (von) ABCMeta ist; zum Beispiel

from abc import ABCMeta

class MyABC(metaclass=ABCMeta):
    pass

MyABC.register(tuple)

assert issubclass(tuple, MyABC)
assert isinstance((), MyABC)

Die letzten beiden Asserts sind äquivalent zu den folgenden beiden

assert MyABC.__subclasscheck__(tuple)
assert MyABC.__instancecheck__(())

Natürlich können Sie auch direkt von `MyABC` ableiten

class MyClass(MyABC):
    pass

assert issubclass(MyClass, MyABC)
assert isinstance(MyClass(), MyABC)

Auch ist ein Tupel natürlich keine MyClass

assert not issubclass(tuple, MyClass)
assert not isinstance((), MyClass)

Sie können eine andere Klasse als Unterklasse von MyClass registrieren

MyClass.register(list)

assert issubclass(list, MyClass)
assert issubclass(list, MyABC)

Sie können auch eine andere ABC registrieren

class AnotherClass(metaclass=ABCMeta):
    pass

AnotherClass.register(basestring)

MyClass.register(AnotherClass)

assert isinstance(str, MyABC)

Der letzte Assert erfordert das Nachverfolgen der folgenden Ober-/Unterklassen-Beziehungen

MyABC -> MyClass (using regular subclassing)
MyClass -> AnotherClass (using registration)
AnotherClass -> basestring (using registration)
basestring -> str (using regular subclassing)

Das Modul abc definiert außerdem einen neuen Dekorator, @abstractmethod, der zur Deklaration abstrakter Methoden verwendet wird. Eine Klasse, die mindestens eine Methode enthält, die mit diesem Dekorator deklariert wurde und noch nicht überschrieben wurde, kann nicht instanziiert werden. Solche Methoden können von der überschreibenden Methode in der Unterklasse aufgerufen werden (mittels super oder direktem Aufruf). Zum Beispiel

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    @abstractmethod
    def foo(self): pass

A()  # raises TypeError

class B(A):
    pass

B()  # raises TypeError

class C(A):
    def foo(self): print(42)

C()  # works

Hinweis: Der Dekorator @abstractmethod sollte nur innerhalb eines Klassenkörpers verwendet werden und nur für Klassen, deren Metaklasse (von) ABCMeta ist. Das dynamische Hinzufügen abstrakter Methoden zu einer Klasse oder der Versuch, den Abstraktionsstatus einer Methode oder Klasse zu ändern, sobald sie erstellt wurde, wird nicht unterstützt. @abstractmethod beeinflusst nur Unterklassen, die durch reguläre Vererbung abgeleitet wurden; „virtuelle Unterklassen“, die mit der Methode register() registriert wurden, sind nicht betroffen.

Implementierung: Der Dekorator @abstractmethod setzt das Funktionsattribut __isabstractmethod__ auf den Wert True. Die Methode ABCMeta.__new__ berechnet das Typattribut __abstractmethods__ als die Menge aller Methodennamen, die ein __isabstractmethod__-Attribut mit dem Wert `True` haben. Dies geschieht durch Kombination der __abstractmethods__-Attribute der Basisklassen, Hinzufügen der Namen aller Methoden im neuen Klassenwörterbuch, die ein wahres __isabstractmethod__-Attribut haben, und Entfernen der Namen aller Methoden im neuen Klassenwörterbuch, die kein wahres __isabstractmethod__-Attribut haben. Wenn die resultierende __abstractmethods__-Menge nicht leer ist, gilt die Klasse als abstrakt, und Versuche, sie zu instanziieren, lösen TypeError aus. (Wenn dies in CPython implementiert wäre, könnte ein internes Flag Py_TPFLAGS_ABSTRACT verwendet werden, um diese Prüfung zu beschleunigen [6].)

Diskussion: Im Gegensatz zu abstrakten Methoden in Java oder reinen abstrakten Methoden in C++ können abstrakte Methoden wie hier definiert auch eine Implementierung haben. Diese Implementierung kann über den super-Mechanismus aus der Klasse aufgerufen werden, die sie überschreibt. Dies könnte als Endpunkt für einen Super-Aufruf in einem Framework nützlich sein, das kooperative Mehrfachvererbung verwendet [7], [8].

Ein zweiter Dekorator, @abstractproperty, wird zur Definition abstrakter Datenattribute definiert. Seine Implementierung ist eine Unterklasse der integrierten Klasse property, die ein __isabstractmethod__-Attribut hinzufügt

class abstractproperty(property):
    __isabstractmethod__ = True

Es kann auf zwei Arten verwendet werden

class C(metaclass=ABCMeta):

    # A read-only property:

    @abstractproperty
    def readonly(self):
        return self.__x

    # A read-write property (cannot use decorator syntax):

    def getx(self):
        return self.__x
    def setx(self, value):
        self.__x = value
    x = abstractproperty(getx, setx)

Ähnlich wie bei abstrakten Methoden kann eine Unterklasse, die eine abstrakte Eigenschaft erbt (deklariert entweder mit der Dekoratorsyntax oder der längeren Form), nicht instanziiert werden, es sei denn, sie überschreibt diese abstrakte Eigenschaft mit einer konkreten Eigenschaft.

ABCs für Container und Iteratoren

Das Modul collections wird die für die Arbeit mit Mengen, Mappings, Sequenzen und einigen Hilfstypen wie Iteratoren und Dictionary-Ansichten notwendigen und ausreichenden ABCs definieren. Alle ABCs haben die oben erwähnte ABCMeta als ihre Metaklasse.

Die ABCs bieten Implementierungen ihrer abstrakten Methoden, die technisch gültig, aber ziemlich nutzlos sind; z. B. gibt __hash__ 0 zurück, und __iter__ gibt einen leeren Iterator zurück. Im Allgemeinen repräsentieren die abstrakten Methoden das Verhalten eines leeren Containers des angegebenen Typs.

Einige ABCs bieten auch konkrete (d. h. nicht-abstrakte) Methoden an; beispielsweise hat die Klasse Iterator eine Methode __iter__, die sich selbst zurückgibt und damit eine wichtige Invariante von Iteratoren erfüllt (die in Python 2 von jeder Iterator-Klasse neu implementiert werden musste). Diese ABCs können als „Mix-in“-Klassen betrachtet werden.

Keine in diesem PEP definierten ABCs überschreiben __init__, __new__, __str__ oder __repr__. Die Definition einer Standardkonstruktorsignatur würde benutzerdefinierte Containertypen, z. B. Patricia-Bäume oder gdbm-Dateien, unnötig einschränken. Die Definition einer spezifischen Zeichenkettendarstellung für eine Sammlung bleibt ebenfalls den einzelnen Implementierungen überlassen.

Hinweis: Es gibt keine ABCs für Ordnungsvorgänge (__lt__, __le__, __ge__, __gt__). Die Definition dieser in einer Basisklasse (abstrakt oder nicht) führt zu Problemen mit dem akzeptierten Typ für den zweiten Operanden. Zum Beispiel würde, wenn die Klasse Ordering __lt__ definierte, angenommen, dass für alle Ordering-Instanzen x und y gilt, dass x < y definiert ist (selbst wenn es nur eine partielle Ordnung definiert). Aber das kann nicht der Fall sein: Wenn sowohl list als auch str von Ordering abgeleitet würden, würde dies bedeuten, dass [1, 2] < (1, 2) definiert sein sollte (und vermutlich False zurückgeben würde), während in der Tat (in Python 3000!) solche „Mixed-Mode-Vergleiche“ explizit verboten sind und TypeError auslösen. Siehe PEP 3100 und [14] für weitere Informationen. (Dies ist ein Sonderfall eines allgemeineren Problems mit Operationen, die ein anderes Argument desselben Typs entgegennehmen.)

One-Trick-Ponys

Diese abstrakten Klassen repräsentieren einzelne Methoden wie __iter__ oder __len__.

Hashable
Die Basisklasse für Klassen, die __hash__ definieren. Die Methode __hash__ sollte eine Ganzzahl zurückgeben. Die abstrakte Methode __hash__ gibt immer 0 zurück, was eine gültige (wenn auch ineffiziente) Implementierung ist. **Invariante:** Wenn die Klassen C1 und C2 beide von Hashable abgeleitet sind, muss die Bedingung o1 == o2 implizieren hash(o1) == hash(o2) für alle Instanzen o1 von C1 und alle Instanzen o2 von C2. Mit anderen Worten, zwei Objekte sollten niemals gleich sein, wenn sie unterschiedliche Hash-Werte haben.

Eine weitere Einschränkung ist, dass hashbare Objekte nach ihrer Erstellung niemals ihren Wert (verglichen mit `==`) oder ihren Hash-Wert ändern sollten. Wenn eine Klasse dies nicht garantieren kann, sollte sie nicht von Hashable abgeleitet werden; wenn sie dies für bestimmte Instanzen nicht garantieren kann, sollte __hash__ für diese Instanzen eine TypeError-Ausnahme auslösen.

Hinweis: Das Sein einer Instanz dieser Klasse impliziert nicht, dass ein Objekt unveränderlich ist; z. B. ist ein Tupel, das eine Liste als Mitglied enthält, nicht unveränderlich; seine __hash__-Methode löst TypeError aus. (Dies liegt daran, dass es rekursiv versucht, den Hash jedes Mitglieds zu berechnen; wenn ein Mitglied nicht hashbar ist, löst es TypeError aus.)

Iterable
Die Basisklasse für Klassen, die __iter__ definieren. Die Methode __iter__ sollte immer eine Instanz von Iterator (siehe unten) zurückgeben. Die abstrakte Methode __iter__ gibt einen leeren Iterator zurück.
Iterator
Die Basisklasse für Klassen, die __next__ definieren. Diese leitet von Iterable ab. Die abstrakte Methode __next__ löst StopIteration aus. Die konkrete Methode __iter__ gibt self zurück. Beachten Sie die Unterscheidung zwischen Iterable und Iterator: Ein Iterable kann iteriert werden, d. h. unterstützt die __iter__-Methoden; ein Iterator ist das, was die eingebaute Funktion iter() zurückgibt, d. h. unterstützt die __next__-Methode.
Sized
Die Basisklasse für Klassen, die __len__ definieren. Die Methode __len__ sollte eine Integer (siehe „Zahlen“ unten) >= 0 zurückgeben. Die abstrakte Methode __len__ gibt 0 zurück. **Invariante:** Wenn eine Klasse C von Sized sowie von Iterable abgeleitet ist, sollte die Invariante sum(1 for x in c) == len(c) für jede Instanz c von C gelten.
Container
Die Basisklasse für Klassen, die __contains__ definieren. Die Methode __contains__ sollte ein bool zurückgeben. Die abstrakte Methode __contains__ gibt False zurück. **Invariante:** Wenn eine Klasse C von Container sowie von Iterable abgeleitet ist, dann sollte (x in c for x in c) ein Generator sein, der nur True-Werte für jede Instanz c von C liefert.

Offene Fragen: Konzeptionell könnten diese Klassen anstelle der Metaklasse ABCMeta __instancecheck__ und __subclasscheck__ überschreiben, um die Anwesenheit der anwendbaren Spezialmethode zu prüfen; zum Beispiel

class Sized(metaclass=ABCMeta):
    @abstractmethod
    def __hash__(self):
        return 0
    @classmethod
    def __instancecheck__(cls, x):
        return hasattr(x, "__len__")
    @classmethod
    def __subclasscheck__(cls, C):
        return hasattr(C, "__bases__") and hasattr(C, "__len__")

Dies hat den Vorteil, keine explizite Registrierung zu erfordern. Die Semantik ist jedoch schwer exakt richtig zu bekommen, angesichts der verwirrenden Semantik von Instanzattributen gegenüber Klassenattributen und dass eine Klasse eine Instanz ihrer Metaklasse ist; die Prüfung auf __bases__ ist nur eine Annäherung an die gewünschte Semantik. **Strohhalm-Mann:** Machen wir es, aber arrangieren wir es so, dass auch die Registrierungs-API funktioniert.

Mengen

Diese abstrakten Klassen repräsentieren schreibgeschützte Mengen und veränderbare Mengen. Die grundlegendste Mengenoperation ist der Mitgliedschaftstest, geschrieben als x in s und implementiert durch s.__contains__(x). Diese Operation ist bereits durch die oben definierte Klasse Container definiert. Daher definieren wir eine Menge als eine größenbezogene, iterierbare Sammlung, für die bestimmte Invarianten der mathematischen Mengentheorie gelten.

Der eingebaute Typ `set` leitet sich von MutableSet ab. Der eingebaute Typ `frozenset` leitet sich von Set und Hashable ab.

Set
Dies ist ein größenbezogener, iterierbarer Container, d. h. eine Unterklasse von Sized, Iterable und Container. Nicht jede Unterklasse dieser drei Klassen ist jedoch eine Menge! Mengen haben die zusätzliche Invariante, dass jedes Element nur einmal vorkommt (wie durch Iteration bestimmt werden kann), und zusätzlich definieren Mengen konkrete Operatoren, die die Ungleichheitsoperationen als Teilmengen-/Obermengentests implementieren. Im Allgemeinen gelten die Invarianten für endliche Mengen in der Mathematik. [11]

Mengen mit unterschiedlichen Implementierungen können sicher, (normalerweise) effizient und korrekt unter Verwendung der mathematischen Definitionen der Teilmengen-/Obermengenoperationen für endliche Mengen verglichen werden. Die Ordnungsvorgänge haben konkrete Implementierungen; Unterklassen können diese aus Geschwindigkeitsgründen überschreiben, sollten aber die Semantik beibehalten. Da Set von Sized abgeleitet ist, kann __eq__ eine Abkürzung nehmen und sofort False zurückgeben, wenn zwei Mengen ungleicher Länge verglichen werden. Ebenso kann __le__ sofort False zurückgeben, wenn die erste Menge mehr Elemente als die zweite Menge hat. Beachten Sie, dass die Mengeninklusion nur eine partielle Ordnung implementiert; z. B. sind {1, 2} und {1, 3} nicht geordnet (alle drei von <, == und > geben für diese Argumente False zurück). Mengen können nicht relativ zu Mappings oder Sequenzen geordnet werden, aber sie können mit diesen auf Gleichheit verglichen werden (und dann vergleichen sie sich immer ungleich).

Diese Klasse definiert auch konkrete Operatoren zur Berechnung der Vereinigung, Schnittmenge, symmetrischen und asymmetrischen Differenz, nämlich __or__, __and__, __xor__ und __sub__. Diese Operatoren sollten Instanzen von Set zurückgeben. Die Standardimplementierungen rufen die überschreibbare Klassenmethode _from_iterable() mit einem iterierbaren Argument auf. Die Standardimplementierung dieser Factory-Methode gibt eine frozenset-Instanz zurück; sie kann überschrieben werden, um eine andere geeignete Set-Unterklasse zurückzugeben.

Schließlich definiert diese Klasse eine konkrete Methode _hash, die den Hashwert aus den Elementen berechnet. Hashbare Unterklassen von Set können __hash__ durch Aufruf von _hash implementieren, oder sie können denselben Algorithmus effizienter neu implementieren; der implementierte Algorithmus sollte jedoch derselbe sein. Derzeit ist der Algorithmus nur durch den Quellcode vollständig spezifiziert [15].

Hinweis: Die Methoden issubset und issuperset, die im Set-Typ in Python 2 zu finden sind, werden nicht unterstützt, da diese größtenteils nur Aliase für __le__ und __ge__ sind.

MutableSet
Dies ist eine Unterklasse von Set, die zusätzliche Operationen zum Hinzufügen und Entfernen von Elementen implementiert. Die unterstützten Methoden haben die Semantik, die vom set-Typ in Python 2 bekannt ist (außer discard, das nach Java modelliert ist)
.add(x)
Abstrakte Methode, die eine bool zurückgibt, welche das Element x hinzufügt, falls es noch nicht im Set vorhanden ist. Sie sollte True zurückgeben, wenn x hinzugefügt wurde, und False, wenn es bereits vorhanden war. Die abstrakte Implementierung wirft NotImplementedError.
.discard(x)
Abstrakte Methode, die eine bool zurückgibt, welche das Element x entfernt, falls es vorhanden ist. Sie sollte True zurückgeben, wenn das Element vorhanden war, und False, wenn es nicht vorhanden war. Die abstrakte Implementierung wirft NotImplementedError.
.pop()
Konkrete Methode, die ein beliebiges Element entfernt und zurückgibt. Wenn das Set leer ist, wirft es KeyError. Die Standardimplementierung entfernt das erste Element, das vom Iterator des Sets zurückgegeben wird.
.toggle(x)
Konkrete Methode, die eine bool zurückgibt, welche x zum Set hinzufügt, falls es nicht vorhanden war, es aber entfernt, falls es vorhanden war. Sie sollte True zurückgeben, wenn x hinzugefügt wurde, und False, wenn es entfernt wurde.
.clear()
Konkrete Methode, die das Set leert. Die Standardimplementierung ruft wiederholt self.pop() auf, bis KeyError gefangen wird. (Hinweis: Dies ist wahrscheinlich viel langsamer als das einfache Erstellen eines neuen Sets, auch wenn eine Implementierung es mit einem schnelleren Ansatz überschreibt; in einigen Fällen ist jedoch die Objektidentität wichtig.)

Dies unterstützt auch die In-Place-mutierenden Operationen |=, &=, ^=, -=. Dies sind konkrete Methoden, deren rechtes Operand ein beliebiges Iterable sein kann, mit Ausnahme von &=, dessen rechtes Operand ein Container sein muss. Diese ABC stellt nicht die benannten Methoden des integrierten konkreten set-Typs bereit, die (fast) die gleichen Operationen durchführen.

Mappings

Diese abstrakten Klassen repräsentieren schreibgeschützte und mutable Mappings. Die Klasse Mapping repräsentiert die gängigste API für schreibgeschützte Mappings.

Der eingebaute Typ dict leitet sich von MutableMapping ab.

Mapping
Eine Unterklasse von Container, Iterable und Sized. Die Schlüssel eines Mappings bilden naturgemäß ein Set. Die (Schlüssel, Wert)-Paare (die Tupel sein müssen) werden auch als Elemente bezeichnet. Die Elemente bilden ebenfalls ein Set. Methoden
.__getitem__(key)
Abstrakte Methode, die den Wert zurückgibt, der key entspricht, oder KeyError wirft. Die Implementierung wirft immer KeyError.
.get(key, default=None)
Konkrete Methode, die self[key] zurückgibt, wenn dies keine KeyError auslöst, und den default-Wert, wenn doch.
.__contains__(key)
Konkrete Methode, die True zurückgibt, wenn self[key] keine KeyError auslöst, und False, wenn doch.
.__len__()
Abstrakte Methode, die die Anzahl der eindeutigen Schlüssel zurückgibt (d. h. die Länge des Schlüssel-Sets).
.__iter__()
Abstrakte Methode, die jeden Schlüssel im Schlüssel-Set genau einmal zurückgibt.
.keys()
Konkrete Methode, die das Schlüssel-Set als Set zurückgibt. Die Standard-Konkretimplementierung gibt eine „Ansicht“ des Schlüssel-Sets zurück (was bedeutet, dass sich der Wert der Ansicht entsprechend ändert, wenn das zugrunde liegende Mapping geändert wird); Unterklassen müssen keine Ansicht zurückgeben, aber sie sollten ein Set zurückgeben.
.items()
Konkrete Methode, die die Elemente als Set zurückgibt. Die Standard-Konkretimplementierung gibt eine „Ansicht“ des Element-Sets zurück; Unterklassen müssen keine Ansicht zurückgeben, aber sie sollten ein Set zurückgeben.
.values()
Konkrete Methode, die die Werte als größenveränderliches, iterierbares Container (kein Set!) zurückgibt. Die Standard-Konkretimplementierung gibt eine „Ansicht“ der Werte des Mappings zurück; Unterklassen müssen keine Ansicht zurückgeben, aber sie sollten einen größenveränderlichen, iterierbaren Container zurückgeben.

Die folgenden Invarianten sollten für jedes Mapping m gelten

len(m.values()) == len(m.keys()) == len(m.items()) == len(m)
[value for value in m.values()] == [m[key] for key in m.keys()]
[item for item in m.items()] == [(key, m[key]) for key in m.keys()]

d.h. das Iterieren über die Elemente, Schlüssel und Werte sollte die Ergebnisse in derselben Reihenfolge zurückgeben.

MutableMapping
Eine Unterklasse von Mapping, die auch einige Standard-mutierende Methoden implementiert. Abstrakte Methoden sind __setitem__, __delitem__. Konkrete Methoden sind pop, popitem, clear, update. Hinweis: setdefault ist *nicht* enthalten. Offene Punkte: Spezifikationen für die Methoden ausarbeiten.

Sequenzen

Diese abstrakten Klassen repräsentieren schreibgeschützte und mutable Sequenzen.

Die integrierten Typen list und bytes leiten sich von MutableSequence ab. Die integrierten Typen tuple und str leiten sich von Sequence und Hashable ab.

Sequence
Eine Unterklasse von Iterable, Sized, Container. Sie definiert eine neue abstrakte Methode __getitem__, die eine etwas komplizierte Signatur hat: wenn sie mit einer Ganzzahl aufgerufen wird, gibt sie ein Element der Sequenz zurück oder wirft IndexError; wenn sie mit einem slice-Objekt aufgerufen wird, gibt sie eine andere Sequence zurück. Die konkrete __iter__-Methode iteriert über die Elemente unter Verwendung von __getitem__ mit den Ganzzahlargumenten 0, 1 und so weiter, bis IndexError geworfen wird. Die Länge sollte gleich der Anzahl der vom Iterator zurückgegebenen Werte sein.

Offene Punkte: Andere Kandidatenmethoden, die alle Standard-konkrete Implementierungen haben können, die nur von __len__ und __getitem__ mit einem Ganzzahlargument abhängen: __reversed__, index, count, __add__, __mul__.

MutableSequence
Eine Unterklasse von Sequence, die einige Standard-mutierende Methoden hinzufügt. Abstrakte mutierende Methoden: __setitem__ (für Ganzzahlindizes sowie Slices), __delitem__ (ebenso), insert. Konkrete mutierende Methoden: append, reverse, extend, pop, remove. Konkrete mutierende Operatoren: +=, *= (diese mutieren das Objekt inplace). Hinweis: Dies definiert nicht sort() – diese muss nur auf echten list-Instanzen vorhanden sein.

Strings

Python 3000 wird wahrscheinlich mindestens zwei integrierte String-Typen haben: Byte-Strings (bytes), die von MutableSequence abgeleitet sind, und (Unicode-)Zeichenketten (str), die von Sequence und Hashable abgeleitet sind.

Offene Punkte: Die Basisinterfaces für diese definieren, damit alternative Implementierungen und Unterklassen wissen, worauf sie sich einlassen. Dies könnte Gegenstand eines neuen PEP oder mehrerer PEPs sein (PEP 358 sollte für den bytes-Typ übernommen werden).

ABCs vs. Alternativen

In diesem Abschnitt werde ich versuchen, ABCs mit anderen vorgeschlagenen Ansätzen zu vergleichen und gegenüberzustellen.

ABCs vs. Duck Typing

Bedeutet die Einführung von ABCs das Ende von Duck Typing? Ich glaube nicht. Python wird nicht verlangen, dass eine Klasse von BasicMapping oder Sequence abgeleitet wird, wenn sie eine __getitem__-Methode definiert, noch wird die x[y]-Syntax verlangen, dass x eine Instanz einer dieser ABCs ist. Sie können immer noch jedes „file-like“-Objekt an sys.stdout zuweisen, solange es eine write-Methode hat.

Natürlich wird es einige Anreize geben, Benutzer zu ermutigen, von den entsprechenden Basisklassen abzuleiten; diese reichen von Standardimplementierungen für bestimmte Funktionalitäten bis hin zu einer verbesserten Möglichkeit, zwischen Mappings und Sequenzen zu unterscheiden. Aber es gibt keine Zwänge. Wenn hasattr(x, "__len__") für Sie funktioniert, großartig! ABCs sind dazu gedacht, Probleme zu lösen, die in Python 2 gar keine gute Lösung haben, wie z. B. die Unterscheidung zwischen Mappings und Sequenzen.

ABCs vs. Generische Funktionen

ABCs sind mit Generic Functions (GFs) kompatibel. Zum Beispiel verwendet meine eigene Generic Functions-Implementierung [4] die Klassen (Typen) der Argumente als Dispatch-Schlüssel, was es abgeleiteten Klassen ermöglicht, Basisklassen zu überschreiben. Da (aus Pythons Sicht) ABCs ganz normale Klassen sind, kann die Verwendung einer ABC in der Standardimplementierung für eine GF sehr angemessen sein. Wenn ich zum Beispiel eine überladene prettyprint-Funktion habe, wäre es sehr sinnvoll, das Pretty-Printing von Sets wie folgt zu definieren

@prettyprint.register(Set)
def pp_set(s):
    return "{" + ... + "}"  # Details left as an exercise

und Implementierungen für spezifische Unterklassen von Set könnten leicht hinzugefügt werden.

Ich glaube, dass ABCs auch keine Probleme für RuleDispatch, Phillips Ebys GF-Implementierung in PEAK [5] darstellen werden.

Natürlich könnten GF-Befürworter behaupten, dass GFs (und konkrete oder Implementierungsklassen) alles sind, was Sie brauchen. Aber selbst sie werden die Nützlichkeit der Vererbung nicht leugnen; und man kann die in diesem PEP vorgeschlagenen ABCs leicht als optionale Implementierungsbasisklassen betrachten; es gibt keine Anforderung, dass alle benutzerdefinierten Mappings von BasicMapping abgeleitet werden müssen.

ABCs vs. Schnittstellen

ABCs sind nicht intrinsisch mit Interfaces inkompatibel, aber es gibt erhebliche Überschneidungen. Vorerst überlasse ich es den Befürwortern von Interfaces, zu erklären, warum Interfaces besser sind. Ich erwarte, dass viel von der Arbeit, die z. B. in die Definition der verschiedenen Abstufungen von „Mapping-Art“ und der Nomenklatur geflossen ist, leicht für einen Vorschlag zur Verwendung von Interfaces anstelle von ABCs adaptiert werden könnte.

„Interfaces“ bezieht sich in diesem Zusammenhang auf eine Reihe von Vorschlägen für zusätzliche Metadaten-Elemente, die einer Klasse zugeordnet sind und nicht Teil der regulären Klassenhierarchie sind, aber bestimmte Arten von Vererbungstests ermöglichen.

Solche Metadaten würden, zumindest in einigen Vorschlägen, so gestaltet sein, dass sie von einer Anwendung leicht geändert werden können, was es Anwendungsentwicklern ermöglicht, die normale Klassifizierung eines Objekts zu überschreiben.

Der Nachteil dieser Idee, mutable Metadaten an eine Klasse anzuhängen, ist, dass Klassen geteilter Zustand sind und ihre Änderung zu Konflikten der Absicht führen kann. Darüber hinaus kann die Notwendigkeit, die Klassifizierung eines Objekts zu überschreiben, mit generischen Funktionen sauberer erledigt werden: Im einfachsten Fall kann eine generische Funktion „Kategorienmitgliedschaft“ definiert werden, die in der Basisimplementierung einfach False zurückgibt, und dann können Überschreibungen bereitgestellt werden, die für alle interessierenden Klassen True zurückgeben.

Referenzen

[2] Unvollständiges Implementierungsprototyp, von GvR (https://web.archive.org/web/20170223133820/http://svn.python.org/view/sandbox/trunk/abc/)

[3] Mögliche Python 3K Klassenhierarchie?, Wiki-Seite erstellt von Bill Janssen (https://wiki.python.org/moin/AbstractBaseClasses)

[9] Partielle Ordnung, in Wikipedia (https://en.wikipedia.org/wiki/Partial_order)

[10] Totale Ordnung, in Wikipedia (https://en.wikipedia.org/wiki/Total_order)


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT