PEP 246 – Objektanpassung
- Autor:
- Alex Martelli <aleaxit at gmail.com>, Clark C. Evans <cce at clarkevans.com>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 21. März 2001
- Python-Version:
- 2.5
- Post-History:
- 29. März 2001, 10. Januar 2005
Inhaltsverzeichnis
- Ablehnungsbescheid
- Zusammenfassung
- Motivation
- Anforderungen
- Spezifikation
- Beabsichtigte Verwendung
- Guidos Blogbeitrag „Optional Static Typing: Stop the Flames“
- Referenzimplementierung und Testfälle
- Beziehung zu Microsofts QueryInterface
- Fragen und Antworten
- Abwärtskompatibilität
- Danksagungen
- Referenzen und Fußnoten
- Urheberrecht
Ablehnungsbescheid
Ich lehne diesen PEP ab. Etwas viel Besseres steht bevor; es ist noch zu früh, um genau zu sagen, was, aber es wird nicht zu sehr dem in diesem PEP vorgeschlagenen ähneln, daher ist es besser, einen neuen PEP zu starten. GvR.
Zusammenfassung
Dieser Vorschlag legt einen erweiterbaren kooperativen Mechanismus zur Anpassung eines eingehenden Objekts an einen Kontext vor, der ein Objekt erwartet, das ein bestimmtes Protokoll unterstützt (z. B. einen bestimmten Typ, eine Klasse oder eine Schnittstelle).
Dieser Vorschlag stellt eine eingebaute Funktion „adapt“ bereit, die für jedes Objekt X und jedes Protokoll Y verwendet werden kann, um die Python-Umgebung nach einer Version von X zu fragen, die Y unterstützt. Hinter den Kulissen fragt der Mechanismus das Objekt X: „Sind Sie jetzt ein Unterstützer des Protokolls Y oder wissen Sie, wie Sie sich selbst verpacken, um einen solchen bereitzustellen?“. Und wenn diese Anfrage fehlschlägt, fragt die Funktion das Protokoll Y: „Unterstützt das Objekt X Sie, oder wissen Sie, wie Sie es verpacken, um einen solchen Unterstützer zu erhalten?“. Diese Dualität ist wichtig, da Protokolle nach Objekten entwickelt werden können oder umgekehrt, und dieser PEP ermöglicht es, beide Fälle nicht-invasiv in Bezug auf die bereits bestehenden Komponenten zu unterstützen.
Schließlich, wenn weder das Objekt noch das Protokoll voneinander wissen, kann der Mechanismus eine Registrierung von Adapterfabriken überprüfen, bei denen aufrufbar Funktionen, die bestimmte Objekte für bestimmte Protokolle anpassen können, dynamisch registriert werden können. Dieser Teil des Vorschlags ist optional: Derselbe Effekt könnte erzielt werden, indem sichergestellt wird, dass bestimmte Arten von Protokollen und/oder Objekten die dynamische Registrierung von Adapterfabriken akzeptieren können, z. B. über geeignete benutzerdefinierte Metaklassen. Dieser optionale Teil ermöglicht jedoch eine flexiblere und leistungsfähigere Anpassung, die weder für Protokolle noch für andere Objekte invasiv ist, wodurch die Anpassung denselben Vorteil erhält, den das Modul „copy_reg“ der Python-Standardbibliothek für Serialisierung und Persistenz bietet.
Dieser Vorschlag schränkt nicht spezifisch ein, was ein Protokoll *ist*, was „Konformität mit einem Protokoll“ genau *bedeutet* oder was genau ein Wrapper tun soll. Diese Auslassungen sollen diesen Vorschlag mit beiden bestehenden Kategorien von Protokollen kompatibel machen, wie dem bestehenden System von Typen und Klassen, sowie den vielen Konzepten für „Schnittstellen“ als solche, die für Python vorgeschlagen oder implementiert wurden, wie z. B. die in PEP 245, die in Zope3 [2], oder die, die im Artima-Blog des BDFL Ende 2004 und Anfang 2005 diskutiert wurden [3]. Einige Überlegungen zu diesen Themen, die suggestiv und nicht normativ gemeint sind, sind ebenfalls enthalten.
Motivation
Derzeit gibt es keinen standardisierten Mechanismus in Python, um zu überprüfen, ob ein Objekt ein bestimmtes Protokoll unterstützt. Typischerweise wird die Existenz bestimmter Methoden, insbesondere spezieller Methoden wie __getitem__, als Indikator für die Unterstützung eines bestimmten Protokolls verwendet. Diese Technik funktioniert gut für einige spezifische Protokolle, die vom BDFL (Benevolent Dictator for Life) gesegnet sind. Dasselbe gilt für die alternative Technik, die auf der Überprüfung von „isinstance“ basiert (die eingebaute Klasse „basestring“ existiert speziell, damit Sie mit „isinstance“ überprüfen können, ob ein Objekt „ein [eingebauter] String ist“). Keiner der Ansätze ist leicht und allgemein auf andere Protokolle erweiterbar, die von Anwendungen und Frameworks von Drittanbietern außerhalb des standardmäßigen Python-Kerns definiert werden.
Noch wichtiger als die Überprüfung, ob ein Objekt ein bestimmtes Protokoll bereits unterstützt, kann die Aufgabe sein, einen geeigneten Adapter (Wrapper oder Proxy) für das Objekt zu erhalten, falls die Unterstützung noch nicht vorhanden ist. Ein String unterstützt beispielsweise nicht das Dateiprotokoll, aber Sie können ihn in eine StringIO-Instanz verpacken, um ein Objekt zu erhalten, das dieses Protokoll unterstützt und seine Daten aus dem String bezieht, den es verpackt; auf diese Weise können Sie den String (angemessen verpackt) an Untersysteme übergeben, die Objekte als Argumente benötigen, die als Dateien lesbar sind. Leider gibt es derzeit keine allgemeine, standardisierte Möglichkeit, diese äußerst wichtige Art von „Anpassung durch Verpackung“ zu automatisieren.
Typischerweise übergibt man heute Objekte an einen Kontext, der ein bestimmtes Protokoll erwartet, entweder kennt das Objekt den Kontext und stellt seinen eigenen Wrapper bereit, oder der Kontext kennt das Objekt und verpackt es entsprechend. Die Schwierigkeit bei diesen Ansätzen besteht darin, dass solche Anpassungen einmalige Ereignisse sind, nicht zentral an einer Stelle im Benutzercode konzentriert sind und nicht mit einer gemeinsamen Technik ausgeführt werden usw. Dieser Mangel an Standardisierung erhöht die Code-Duplizierung, da derselbe Adapter an mehreren Stellen auftritt, oder er ermutigt Klassen, neu geschrieben zu werden, anstatt angepasst zu werden. In beiden Fällen leidet die Wartbarkeit.
Es wäre sehr wünschenswert, eine Standardfunktion zu haben, die aufgerufen werden kann, um die Konformität eines Objekts mit einem bestimmten Protokoll zu überprüfen und bei Bedarf einen Wrapper bereitzustellen – und das alles, ohne die Dokumentation jeder Bibliothek nach der richtigen Formulierung für diesen speziellen Fall durchsuchen zu müssen.
Anforderungen
Bei der Betrachtung der Konformität eines Objekts mit einem Protokoll sind mehrere Fälle zu untersuchen
- Wenn das Protokoll ein Typ oder eine Klasse ist und das Objekt genau diesen Typ hat oder eine Instanz genau dieser Klasse ist (nicht eine Unterklasse). In diesem Fall ist die Konformität automatisch.
- Wenn das Objekt das Protokoll kennt und sich entweder als konform betrachtet oder weiß, wie es sich selbst geeignet verpacken kann.
- Wenn das Protokoll das Objekt kennt und entweder das Objekt bereits konform ist oder das Protokoll weiß, wie es das Objekt geeignet verpacken kann.
- Wenn das Protokoll ein Typ oder eine Klasse ist und das Objekt ein Mitglied einer Unterklasse ist. Dies ist vom ersten Fall (a) oben zu unterscheiden, da Vererbung (leider) nicht unbedingt Substituierbarkeit impliziert und daher sorgfältig behandelt werden muss.
- Wenn der Kontext das Objekt und das Protokoll kennt und weiß, wie das Objekt so angepasst werden kann, dass das erforderliche Protokoll erfüllt wird. Dies könnte eine Adapterregistrierung oder ähnliche Ansätze verwenden.
Der vierte Fall oben ist subtil. Ein Bruch der Substituierbarkeit kann auftreten, wenn eine Unterklasse die Signatur einer Methode ändert oder die für Argumente einer Methode akzeptierten Domänen einschränkt („Ko-Varianz“ bei Argumenttypen) oder die Ko-Domäne um Rückgabewerte erweitert, die die Basisklasse möglicherweise nie erzeugt („Kontra-Varianz“ bei Rückgabetypen). Obwohl die Konformität basierend auf Klassenerbung automatisch sein *sollte*, erlaubt dieser Vorschlag einem Objekt zu signalisieren, dass es nicht mit einem Basisklassenprotokoll konform ist.
Wenn Python einen standardmäßigen „offiziellen“ Mechanismus für Schnittstellen erhält, dann kann und sollte der „Schnellweg“-Fall (a) auf das Protokoll als Schnittstelle und das Objekt als Instanz eines Typs oder einer Klasse erweitert werden, die Konformität mit dieser Schnittstelle beansprucht. Wenn beispielsweise das in [3] diskutierte „interface“-Schlüsselwort in Python übernommen wird, könnte der „Schnellweg“ von Fall (a) verwendet werden, da instanziierbare Klassen, die eine Schnittstelle implementieren, die Substituierbarkeit nicht brechen dürften.
Spezifikation
Dieser Vorschlag führt eine neue eingebaute Funktion ein, adapt(), die die Grundlage für die Erfüllung dieser Anforderungen bildet.
Die Funktion adapt() hat drei Parameter
obj, das anzupassende Objektprotocol, das vom Objekt geforderte Protokollalternate, ein optionales Objekt, das zurückgegeben wird, wenn das Objekt nicht angepasst werden konnte
Ein erfolgreiches Ergebnis der Funktion adapt() gibt entweder das übergebene Objekt obj zurück, wenn das Objekt bereits mit dem Protokoll konform ist, oder ein sekundäres Objekt wrapper, das eine Ansicht des Objekts bietet, die mit dem Protokoll konform ist. Die Definition von Wrapper ist bewusst vage gehalten, und ein Wrapper darf bei Bedarf ein vollständiges Objekt mit eigenem Zustand sein. Die Designabsicht ist jedoch, dass ein Anpassungs-Wrapper eine Referenz auf das ursprüngliche Objekt, das er umschließt, halten sollte, plus (falls erforderlich) ein Minimum an zusätzlichem Zustand, das er nicht an das umwickelte Objekt delegieren kann.
Ein ausgezeichnetes Beispiel für einen Anpassungs-Wrapper ist eine Instanz von StringIO, die einen eingehenden String anpasst, um ihn so zu lesen, als wäre er eine Textdatei: Der Wrapper hält eine Referenz auf den String, kümmert sich aber selbst um den „aktuellen Lesepunkt“ (von *wo* aus dem umwickelten String die Zeichen für den nächsten, z. B. „readline“-Aufruf stammen sollen), da er sie nicht an das umwickelte Objekt delegieren kann (ein String hat kein Konzept von „aktueller Lesepunkt“ oder etwas auch nur annähernd Ähnlichem).
Ein Fehler bei der Anpassung des Objekts an das Protokoll löst eine AdaptationError aus (die eine Unterklasse von TypeError ist), es sei denn, der Parameter alternate wird verwendet, in diesem Fall wird das Argument alternate stattdessen zurückgegeben.
Um den ersten in den Anforderungen aufgeführten Fall zu ermöglichen, prüft die Funktion adapt() zunächst, ob der Typ des Objekts oder die Klasse des Objekts identisch mit dem Protokoll sind. Wenn dies der Fall ist, gibt die Funktion adapt() das Objekt ohne weitere Umstände direkt zurück.
Um den zweiten Fall zu ermöglichen, wenn das Objekt das Protokoll kennt, muss das Objekt eine Methode __conform__() haben. Diese optionale Methode nimmt zwei Argumente an
self, das anzupassende Objektprotocol, das angeforderte Protokoll
Genau wie jede andere spezielle Methode im heutigen Python ist __conform__ dafür vorgesehen, von der Klasse des Objekts übernommen zu werden, nicht vom Objekt selbst (für alle Objekte, außer Instanzen von „klassischen Klassen“, solange wir letztere noch unterstützen müssen). Dies ermöglicht die Hinzufügung eines möglichen „tp_conform“-Slots zu den Typobjekten von Python in der Zukunft, falls gewünscht.
Das Objekt kann sich selbst als Ergebnis von __conform__ zurückgeben, um die Konformität anzuzeigen. Alternativ hat das Objekt auch die Möglichkeit, ein konformes Wrapper-Objekt zurückzugeben. Wenn das Objekt weiß, dass es nicht konform ist, obwohl es zu einem Typ gehört, der eine Unterklasse des Protokolls ist, sollte __conform__ eine LiskovViolation Ausnahme (eine Unterklasse von AdaptationError) auslösen. Wenn das Objekt seine Konformität nicht bestimmen kann, sollte es None zurückgeben, um die verbleibenden Mechanismen zu aktivieren. Wenn __conform__ eine andere Ausnahme auslöst, gibt „adapt“ diese einfach weiter.
Um den dritten Fall zu ermöglichen, wenn das Protokoll das Objekt kennt, muss das Protokoll eine Methode __adapt__() haben. Diese optionale Methode nimmt zwei Argumente an
self, das angeforderte Protokollobj, das angepasste Objekt
Wenn das Protokoll das Objekt als konform ansieht, kann es obj direkt zurückgeben. Alternativ kann die Methode einen Wrapper zurückgeben, der mit dem Protokoll konform ist. Wenn das Protokoll weiß, dass das Objekt nicht konform ist, obwohl es zu einem Typ gehört, der eine Unterklasse des Protokolls ist, sollte __adapt__ eine LiskovViolation Ausnahme (eine Unterklasse von AdaptationError) auslösen. Wenn die Konformität nicht bestimmt werden kann, sollte diese Methode schließlich None zurückgeben, um die verbleibenden Mechanismen zu aktivieren. Wenn __adapt__ eine andere Ausnahme auslöst, gibt „adapt“ diese einfach weiter.
Der vierte Fall, bei dem die Klasse des Objekts eine Unterklasse des Protokolls ist, wird von der eingebauten Funktion adapt() behandelt. Unter normalen Umständen, wenn „isinstance(object, protocol)“, gibt adapt() das Objekt direkt zurück. Wenn das Objekt jedoch nicht substituierbar ist, können entweder die Methoden __conform__() oder __adapt__(), wie oben erwähnt, eine LiskovViolation (eine Unterklasse von AdaptationError) auslösen, um dieses Standardverhalten zu verhindern.
Wenn keiner der ersten vier Mechanismen funktionierte, greift „adapt“ als letzte Möglichkeit auf die Überprüfung einer Registrierung von Adapterfabriken zurück, die nach dem Protokoll und dem Typ von obj indiziert ist, um den fünften Fall zu erfüllen. Adapterfabriken können dynamisch aus dieser Registrierung registriert und entfernt werden, um „Anpassungen von Drittanbietern“ von Objekten und Protokollen zu ermöglichen, die sich gegenseitig nicht kennen, auf eine Weise, die weder für das Objekt noch für die Protokolle invasiv ist.
Beabsichtigte Verwendung
Die typische beabsichtigte Verwendung von adapt ist in Code, der ein Objekt X „von außen“ erhalten hat, entweder als Argument oder als Ergebnis des Aufrufs einer Funktion, und dieses Objekt gemäß einem bestimmten Protokoll Y verwenden muss. Ein „Protokoll“ wie Y soll eine Schnittstelle angeben, die in der Regel mit einigen semantischen Einschränkungen angereichert ist (wie sie typischerweise im Ansatz „Design by Contract“ verwendet werden) und oft auch einige pragmatische Erwartungen (wie „die Laufzeit einer bestimmten Operation sollte nicht schlechter sein als O(N)“ oder Ähnliches); dieser Vorschlag legt nicht fest, wie Protokolle als solche gestaltet sind, noch wie oder ob die Konformität mit einem Protokoll überprüft wird, noch welche Folgen die Beanspruchung der Konformität ohne tatsächliche Lieferung haben kann (mangelnde „syntaktische“ Konformität – Namen und Signaturen von Methoden – führt oft dazu, dass Ausnahmen ausgelöst werden; mangelnde „semantische“ Konformität kann zu subtilen und vielleicht gelegentlichen Fehlern führen [stellen Sie sich eine Methode vor, die beansprucht, threadsicher zu sein, aber tatsächlich einer subtilen Race-Condition unterliegt]; mangelnde „pragmatische“ Konformität führt im Allgemeinen zu Code, der *korrekt* läuft, aber zu langsam für den praktischen Gebrauch ist, oder manchmal zur Erschöpfung von Ressourcen wie Speicher oder Festplattenspeicher).
Wenn das Protokoll Y ein konkreter Typ oder eine Klasse ist, bedeutet Konformität damit, dass ein Objekt alle Operationen zulässt, die auf Instanzen von Y ausgeführt werden könnten, mit „vergleichbarer“ Semantik und Pragmatik. Beispielsweise sollte ein hypothetisches Objekt X, das eine einfach verkettete Liste ist, keine Konformität mit dem Protokoll ‚list‘ beanspruchen, auch wenn es alle Methoden von list implementiert: Die Tatsache, dass die Indizierung X[n] die Zeit O(n) benötigt, während dieselbe Operation bei einer Liste O(1) wäre, macht einen Unterschied. Andererseits erfüllt eine Instanz von StringIO.StringIO das Protokoll ‚file‘, auch wenn einige Operationen (wie die des Moduls ‚marshal‘) möglicherweise nicht erlauben, eine für die andere zu ersetzen, da sie explizite Typüberprüfungen durchführen: solche Typüberprüfungen liegen aus der Sicht der Protokollkonformität „außerhalb des Rahmens“.
Während diese Konvention es praktikabel macht, einen konkreten Typ oder eine Klasse als Protokoll für die Zwecke dieses Vorschlags zu verwenden, ist eine solche Verwendung oft nicht optimal. Selten benötigt der Code, der ‚adapt‘ aufruft, ALLE Funktionen eines bestimmten konkreten Typs, insbesondere für so reichhaltige Typen wie Datei, Liste, Wörterbuch; selten können alle diese Funktionen von einem Wrapper mit guter Pragmatik sowie Syntax und Semantik bereitgestellt werden, die tatsächlich dieselben wie die eines konkreten Typs sind.
Vielmehr muss nach Annahme dieses Vorschlags eine Designanstrengung unternommen werden, um die wesentlichen Merkmale der Protokolle zu identifizieren, die derzeit in Python verwendet werden, insbesondere innerhalb der Standardbibliothek, und sie mit einer Art „Schnittstellen“-Konstrukt zu formalisieren (nicht unbedingt mit neuer Syntax: eine einfache benutzerdefinierte Metaklasse würde uns den Einstieg ermöglichen, und die Ergebnisse der Anstrengung könnten später in jedes „Schnittstellen“-Konstrukt migriert werden, das schließlich in die Python-Sprache aufgenommen wird). Mit einer solchen Palette von formaler gestalteten Protokollen kann der Code, der ‚adapt‘ verwendet, beispielsweise nach einer Anpassung an „ein dateiähnliches Objekt, das lesbar und suchbar ist“ fragen, oder was auch immer er speziell benötigt, mit einem angemessenen Grad an „Granularität“, anstatt zu generisch nach Konformität mit dem ‚file‘-Protokoll zu fragen.
Anpassung ist KEIN „Casting“. Wenn Objekt X selbst nicht mit Protokoll Y konform ist, bedeutet die Anpassung von X an Y die Verwendung einer Art Wrapper-Objekt Z, das eine Referenz auf X enthält und die von Y geforderten Operationen implementiert, meist durch geeignete Delegation an X. Wenn X beispielsweise ein String ist und Y ‚file‘ ist, ist der richtige Weg, X an Y anzupassen, ein StringIO(X) zu erstellen, **NICHT** file(X) aufzurufen [was versuchen würde, eine Datei mit dem Namen X zu öffnen].
Numerische Typen und Protokolle müssen möglicherweise eine Ausnahme von diesem Mantra „Anpassung ist kein Casting“ bilden.
Guidos Blogbeitrag „Optional Static Typing: Stop the Flames“
Ein typischer einfacher Anwendungsfall für Anpassung wäre
def f(X):
X = adapt(X, Y)
# continue by using X according to protocol Y
In [4] hat der BDFL die Einführung der Syntax vorgeschlagen
def f(X: Y):
# continue by using X according to protocol Y
als praktische Abkürzung für genau diese typische Verwendung von adapt und, als Grundlage für Experimente, bis der Parser geändert wurde, um diese neue Syntax zu akzeptieren, einen semantisch äquivalenten Dekorator
@arguments(Y)
def f(X):
# continue by using X according to protocol Y
Diese BDFL-Ideen sind vollständig kompatibel mit diesem Vorschlag, ebenso wie andere von Guidos Vorschlägen im selben Blog.
Referenzimplementierung und Testfälle
Die folgende Referenzimplementierung befasst sich nicht mit klassischen Klassen: sie berücksichtigt nur neue Klassen. Wenn klassische Klassen unterstützt werden müssen, sollten die Ergänzungen ziemlich klar, aber etwas unordentlich sein (x.__class__ vs. type(x), direkte Erlangung von gebundenen Methoden vom Objekt anstatt vom Typ und so weiter).
-----------------------------------------------------------------
adapt.py
-----------------------------------------------------------------
class AdaptationError(TypeError):
pass
class LiskovViolation(AdaptationError):
pass
_adapter_factory_registry = {}
def registerAdapterFactory(objtype, protocol, factory):
_adapter_factory_registry[objtype, protocol] = factory
def unregisterAdapterFactory(objtype, protocol):
del _adapter_factory_registry[objtype, protocol]
def _adapt_by_registry(obj, protocol, alternate):
factory = _adapter_factory_registry.get((type(obj), protocol))
if factory is None:
adapter = alternate
else:
adapter = factory(obj, protocol, alternate)
if adapter is AdaptationError:
raise AdaptationError
else:
return adapter
def adapt(obj, protocol, alternate=AdaptationError):
t = type(obj)
# (a) first check to see if object has the exact protocol
if t is protocol:
return obj
try:
# (b) next check if t.__conform__ exists & likes protocol
conform = getattr(t, '__conform__', None)
if conform is not None:
result = conform(obj, protocol)
if result is not None:
return result
# (c) then check if protocol.__adapt__ exists & likes obj
adapt = getattr(type(protocol), '__adapt__', None)
if adapt is not None:
result = adapt(protocol, obj)
if result is not None:
return result
except LiskovViolation:
pass
else:
# (d) check if object is instance of protocol
if isinstance(obj, protocol):
return obj
# (e) last chance: try the registry
return _adapt_by_registry(obj, protocol, alternate)
-----------------------------------------------------------------
test.py
-----------------------------------------------------------------
from adapt import AdaptationError, LiskovViolation, adapt
from adapt import registerAdapterFactory, unregisterAdapterFactory
import doctest
class A(object):
'''
>>> a = A()
>>> a is adapt(a, A) # case (a)
True
'''
class B(A):
'''
>>> b = B()
>>> b is adapt(b, A) # case (d)
True
'''
class C(object):
'''
>>> c = C()
>>> c is adapt(c, B) # case (b)
True
>>> c is adapt(c, A) # a failure case
Traceback (most recent call last):
...
AdaptationError
'''
def __conform__(self, protocol):
if protocol is B:
return self
class D(C):
'''
>>> d = D()
>>> d is adapt(d, D) # case (a)
True
>>> d is adapt(d, C) # case (d) explicitly blocked
Traceback (most recent call last):
...
AdaptationError
'''
def __conform__(self, protocol):
if protocol is C:
raise LiskovViolation
class MetaAdaptingProtocol(type):
def __adapt__(cls, obj):
return cls.adapt(obj)
class AdaptingProtocol:
__metaclass__ = MetaAdaptingProtocol
@classmethod
def adapt(cls, obj):
pass
class E(AdaptingProtocol):
'''
>>> a = A()
>>> a is adapt(a, E) # case (c)
True
>>> b = A()
>>> b is adapt(b, E) # case (c)
True
>>> c = C()
>>> c is adapt(c, E) # a failure case
Traceback (most recent call last):
...
AdaptationError
'''
@classmethod
def adapt(cls, obj):
if isinstance(obj, A):
return obj
class F(object):
pass
def adapt_F_to_A(obj, protocol, alternate):
if isinstance(obj, F) and issubclass(protocol, A):
return obj
else:
return alternate
def test_registry():
'''
>>> f = F()
>>> f is adapt(f, A) # a failure case
Traceback (most recent call last):
...
AdaptationError
>>> registerAdapterFactory(F, A, adapt_F_to_A)
>>> f is adapt(f, A) # case (e)
True
>>> unregisterAdapterFactory(F, A)
>>> f is adapt(f, A) # a failure case again
Traceback (most recent call last):
...
AdaptationError
>>> registerAdapterFactory(F, A, adapt_F_to_A)
'''
doctest.testmod()
Beziehung zu Microsofts QueryInterface
Obwohl dieser Vorschlag einige Ähnlichkeiten mit Microsofts (COM) QueryInterface aufweist, unterscheidet er sich in vielerlei Hinsicht.
Erstens ist die Anpassung in diesem Vorschlag bidirektional und ermöglicht es auch, die Schnittstelle (das Protokoll) abzufragen, was mehr dynamische Fähigkeiten (mehr Python-typisch) bietet. Zweitens gibt es keine spezielle „IUnknown“-Schnittstelle, mit der die ursprüngliche unverpackte Objektidentität überprüft oder abgerufen werden kann, obwohl dies als eine dieser „speziellen“, gesegneten Schnittstellenprotokollbezeichner vorgeschlagen werden könnte. Drittens muss bei QueryInterface ein Objekt, das einmal ein bestimmtes Interface unterstützt, dieses Interface danach immer unterstützen; dieser Vorschlag macht keine solche Garantie, da insbesondere Adapterfabriken dynamisch zur Registrierung hinzugefügt und später wieder entfernt werden können.
Viertens müssen Implementierungen von Microsofts QueryInterface eine Art Äquivalenzrelation unterstützen – sie müssen reflexiv, symmetrisch und transitiv in spezifischen Bedeutungen sein. Die äquivalenten Bedingungen für die Protokollanpassung gemäß diesem Vorschlag würden ebenfalls wünschenswerte Eigenschaften darstellen
# given, to start with, a successful adaptation:
X_as_Y = adapt(X, Y)
# reflexive:
assert adapt(X_as_Y, Y) is X_as_Y
# transitive:
X_as_Z = adapt(X, Z, None)
X_as_Y_as_Z = adapt(X_as_Y, Z, None)
assert (X_as_Y_as_Z is None) == (X_as_Z is None)
# symmetrical:
X_as_Z_as_Y = adapt(X_as_Z, Y, None)
assert (X_as_Y_as_Z is None) == (X_as_Z_as_Y is None)
Diese Eigenschaften sind jedoch zwar wünschenswert, aber nicht immer zu garantieren. QueryInterface kann ihre Äquivalente auferlegen, da es diktiert, bis zu einem gewissen Grad, wie Objekte, Schnittstellen und Adapter kodiert werden sollen; dieser Vorschlag soll nicht unbedingt invasiv sein, benutzbar und zur „Nachrüstung“ der Anpassung zwischen zwei Frameworks, die in gegenseitiger Unkenntnis voneinander kodiert sind, ohne eines der Frameworks ändern zu müssen.
Die Transitivität der Anpassung ist in der Tat etwas umstritten, ebenso wie die Beziehung (falls vorhanden) zwischen Anpassung und Vererbung.
Letzteres wäre nicht umstritten, wenn wir wüssten, dass Vererbung immer Liskov-Substituierbarkeit impliziert, was wir leider nicht tun. Wenn eine spezielle Form, wie die in [4] vorgeschlagenen Schnittstellen, tatsächlich Liskov-Substituierbarkeit sicherstellen könnte, dann könnten wir für diese Art von Vererbung, und nur für diese, vielleicht behaupten, dass wenn X mit Y konform ist und Y von Z erbt, dann X mit Z konform ist… aber nur, wenn Substituierbarkeit in einem sehr starken Sinne verstanden wird, der Semantik und Pragmatik einschließt, was zweifelhaft erscheint. (Was es wert ist: In QueryInterface erfordert Vererbung keine Konformität und impliziert sie auch nicht). Dieser Vorschlag beinhaltet keine „starken“ Auswirkungen der Vererbung, über die geringfügigen, die oben spezifisch detailliert sind.
Ähnlich könnte Transitivität mehrere „interne“ Anpassungsdurchläufe implizieren, um das Ergebnis von adapt(X, Z) über ein geeignetes und automatisch gewähltes Y zu erhalten, intrinsisch wie adapt(adapt(X, Y), Z). Auch dies mag unter entsprechend starken Einschränkungen machbar sein, aber die praktischen Auswirkungen eines solchen Schemas sind den Autoren dieses Vorschlags noch unklar. Daher beinhaltet dieser Vorschlag keine automatische oder implizite Transitivität der Anpassung, unter welchen Umständen auch immer.
Für eine Implementierung der ursprünglichen Version dieses Vorschlags, die fortschrittlichere Verarbeitungen in Bezug auf Transitivität und die Auswirkungen von Vererbung durchführt, siehe Phillip J. Ebys PyProtocols [5]. Die Dokumentation, die PyProtocols begleitet, ist gut lesenswert für ihre Überlegungen dazu, wie Adapter kodiert und verwendet werden sollten und wie Anpassung jeglichen Bedarf an Typüberprüfungen im Anwendungscode beseitigen kann.
Fragen und Antworten
- F: Welchen Nutzen bietet dieser Vorschlag?
A: Der typische Python-Programmierer ist ein Integrator, jemand, der Komponenten von verschiedenen Anbietern verbindet. Oft benötigt man zur Schnittstellenbildung zwischen diesen Komponenten Zwischenadapter. Normalerweise liegt die Last auf dem Programmierer, die von einer Komponente bereitgestellte und von einer anderen geforderte Schnittstelle zu studieren, festzustellen, ob sie direkt kompatibel sind, oder einen Adapter zu entwickeln. Manchmal kann ein Anbieter den entsprechenden Adapter sogar mitliefern, aber selbst dann kostet die Suche nach dem Adapter und das Verständnis, wie der Adapter einzusetzen ist, Zeit.
Diese Technik ermöglicht es Anbietern, direkt miteinander zu arbeiten, indem sie nach Bedarf
__conform__oder__adapt__implementieren. Dies entlastet den Integrator von der Erstellung eigener Adapter. Im Wesentlichen ermöglicht dies den Komponenten, einen einfachen Dialog untereinander zu führen. Der Integrator verbindet einfach eine Komponente mit einer anderen, und wenn die Typen nicht automatisch übereinstimmen, ist ein Anpassungsmechanismus eingebaut.Darüber hinaus kann dank der Adapterregistrierung ein „Vierter“ Adapter bereitstellen, um die Interoperabilität von Frameworks zu ermöglichen, die sich gegenseitig völlig unbekannt sind, nicht-invasiv und ohne dass der Integrator mehr tun muss, als die entsprechenden Adapterfabriken beim Start in die Registrierung zu installieren.
Solange Bibliotheken und Frameworks mit der hier vorgeschlagenen Anpassungsinfrastruktur kooperieren (im Wesentlichen durch angemessenes Definieren und Verwenden von Protokollen und Aufrufen von ‚adapt‘ bei erhaltenen Argumenten und Rückgaben von Callback-Factory-Funktionen), wird die Arbeit des Integrators dadurch erheblich vereinfacht.
Betrachten Sie beispielsweise die SAX1- und SAX2-Schnittstellen: Es ist ein Adapter erforderlich, um zwischen ihnen zu wechseln. Normalerweise muss der Programmierer sich dessen bewusst sein; mit diesem Anpassungsvorschlag ist dies jedoch nicht mehr der Fall – tatsächlich kann dieser Bedarf dank der Adapterregistrierung sogar entfallen, selbst wenn das Framework, das SAX1 bereitstellt, und dasjenige, das SAX2 benötigt, sich nicht kennen.
- F: Warum muss dies eingebaut sein, kann es nicht eigenständig sein?
A: Ja, es funktioniert eigenständig. Wenn es jedoch eingebaut ist, hat es eine größere Chance, genutzt zu werden. Der Wert dieses Vorschlags liegt hauptsächlich in der Standardisierung: Bibliotheken und Frameworks von verschiedenen Anbietern, einschließlich der Python-Standardbibliothek, verwenden einen einzigen Ansatz zur Anpassung. Darüber hinaus
- Der Mechanismus ist von Natur aus ein Singleton.
- Wenn er häufig verwendet wird, ist er als eingebautes Element viel schneller.
- Er ist erweiterbar und unaufdringlich.
- Sobald ‚adapt‘ eingebaut ist, kann es Syntaxerweiterungen unterstützen und sogar einem Typinferenzsystem helfen.
- F: Warum die Verben
__conform__und__adapt__?A: conform, intransitives Verb
- Entsprechen in Form oder Charakter; ähnlich sein.
- Übereinstimmen oder übereinstimmen; befolgen.
- Entsprechend aktuellen Bräuchen oder Modi handeln.
adapt, transitives Verb
- Zur Eignung oder zum Einpassen für einen bestimmten Gebrauch oder eine Situation machen.
Quelle: The American Heritage Dictionary of the English Language, Third Edition
Abwärtskompatibilität
Es sollte keine Probleme mit der Abwärtskompatibilität geben, es sei denn, jemand hätte die speziellen Namen __conform__ oder __adapt__ auf andere Weise verwendet, aber das scheint unwahrscheinlich, und auf jeden Fall sollte Benutzercode niemals spezielle Namen für nicht standardmäßige Zwecke verwenden.
Dieser Vorschlag könnte ohne Änderungen am Interpreter implementiert und getestet werden.
Danksagungen
Dieser Vorschlag wurde zu einem großen Teil durch das Feedback der talentierten Individuen auf den wichtigsten Python-Mailinglisten und der type-sig-Liste erstellt. Um spezifische Mitwirkende zu nennen (mit Entschuldigung, falls wir jemanden übersehen haben!), abgesehen von den Autoren des Vorschlags: Die Hauptempfehlungen für die ersten Versionen des Vorschlags kamen von Paul Prescod, mit erheblichem Feedback von Robin Thomas, und wir haben auch Ideen von Marcin ‚Qrczak‘ Kowalczyk und Carlos Ribeiro übernommen.
Weitere Mitwirkende (über Kommentare) sind Michel Pelletier, Jeremy Hylton, Aahz Maruch, Fredrik Lundh, Rainer Deyke, Timothy Delaney und Huaiyu Zhu. Die aktuelle Version verdankt viel den Diskussionen mit (unter anderem) Phillip J. Eby, Guido van Rossum, Bruce Eckel, Jim Fulton und Ka-Ping Yee sowie dem Studium und der Reflexion ihrer Vorschläge, Implementierungen und Dokumentationen über die Verwendung und Anpassung von Schnittstellen und Protokollen in Python.
Referenzen und Fußnoten
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0246.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT