From be2162836ab2d38159fe687e9535150cc8e69d14 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 29 Mar 2001 03:12:47 +0000 Subject: [PATCH] PEP 246, Object Adaptation, Clark C. Evans (with editing for style, spell-checking, etc. by Barry) --- pep-0246.txt | 546 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 pep-0246.txt diff --git a/pep-0246.txt b/pep-0246.txt new file mode 100644 index 000000000..d15a14c8c --- /dev/null +++ b/pep-0246.txt @@ -0,0 +1,546 @@ +PEP: 246 +Title: Object Adaptation +Version: $Revision$ +Author: cce@clarkevans.com (Clark C. Evans) +Status: Draft +Type: Standards Track +Created: 21-Mar-2001 +Python-Version: 2.2 + + +Abstract + + This proposal puts forth an extensible mechanism for the + adaptation of an object to a context where a specific type, class, + interface, or other protocol is expected. + + This proposal provides a built-in "adapt" function that, for any + object X and protocol Y, can be used to ask the Python environment + for a version of X complaint with Y. Behind the scenes, the + mechanism asks the object X: "Are you now, or do you know how to + wrap yourself to provide, a supporter of protocol Y?". And, if + this request fails, the function then asks the protocol Y: "Does + object X support you, or do you know how to wrap it to obtain such + a supporter?" This duality is important, because protocols can be + developed after objects are, or vice-versa, and this PEP lets + either case be supported non-invasively with regard to the + pre-existing component[s]. + + This proposal does not limit what a protocol is, what compliance + to the protocol means, nor what a wrapper constitutes. This + mechanism leverages existing protocol categories such as the type + system and class hierarchy and can be expanded to support future + protocol categories such as the pending interface proposal [1] and + signature based type-checking system [2]. + + +Motivation + + Currently there is no standardized mechanism in Python for asking + if an object supports a particular protocol. Typically, existence + of particular methods, particularly those that are built-in such + as __getitem__, is used as an indicator of support for a + particular protocol. This technique works for protocols blessed + by the BDFL (Benevolent Dictator for Life), such as the new + enumerator proposal identified by a new built-in __iter__[9]. + However, this technique does not admit an infallible way to + identify interfaces lacking a unique, built-in signature method. + + More so, there is no standardized way to obtain an adapter for an + object. Typically, with objects passed to a context expecting a + particular protocol, either the object knows about the context and + provides its own wrapper or the context knows about the object and + wraps it appropriately. The difficulty with these approaches is + that such adaptations are one-offs, are not centralized in a + single place of the users code, and are not executed with a common + technique, etc. This lack of standardization increases code + duplication with the same adapter occurring in more than one place + or it encourages classes to be re-written instead of adapted. In + either case, maintainability suffers. + + It would be very nice to have a standard function that can be + called upon to verify an object's compliance with a particular + protocol and provide for a wrapper if one is readily available -- + all without having to hunt through a library's documentation for + the appropriate incantation. + + +Requirements + + When considering an objects compliance with a protocol, there are + several cases to be examined: + + a) When the protocol is a type or class, and the object has + exactly that type or is a member of the class. In this case + compliance is automatic. + + b) When the object knows about the protocol and either considers + itself compliant or knows how to wrap itself appropriately. + + c) When the protocol knows about the object and either the object + already complies or can be wrapped accordingly. + + d) When the protocol is a class, and the object is a member of a + subclass. This is distinct from the first case (a) above, + since inheritance does not necessarily imply substitutability + and must be handled carefully. + + e) When the context knows about the object and the protocol and + knows how to adapt the object so that the required protocol is + satisfied. This could use an adapter registry or similar + method. + + For this proposal's requirements, the first case should be come + for free and the next three cases should be relatively relatively + easy to accomplish. This proposal does not address the last case, + however it provides a base mechanism upon which such an approach + could be developed. Further, with only minor implementation + changes, this proposal should be able to incorporate a new + interface type or type checking system. + + The fourth case above is subtle. A lack of substitutability can + occur when a method restricts an argument's domain or raises an + exception which a base class does not or extends the co-domain to + include return values which the base class may never produce. + While compliance based on class inheritance should be automatic, + this proposal should allow an object to signal that it is not + compliant with a base class protocol. + + +Specification + + This proposal introduces a new built-in function, adapt(), which + is the basis for supporting these requirements. + + The adapt() function has three parameters: + + - `obj', the object to be adapted + + - `protocol', the protocol requested of the object + + - `alternate', an optional object to return if the object could + not be adapted + + A successful result of the adapt() function returns either the + object passed `obj' if the object is already compliant with the + protocol, or a secondary object `wrapper', which provides a view + of the object compliant with the protocol. The definition of + wrapper is explicitly vague and a wrapper is allowed to be a full + object with its own state if necessary. A failure to adapt the + object to the protocol will raise a TypeError unless the alternate + parameter is used, in this case the alternate argument is + returned. + + To enable the first case listed in the requirements, the adapt() + function first checks to see if the object's type or the object's + class are identical to the protocol. If so, then the adapt() + function returns the object directly without further ado. + + To enable the second case, when the object knows about the + protocol, the object must have a __conform__() method. This + optional method takes two arguments: + + - `self', the object being conformed + + - `protocol, the protocol requested + + The object may return itself through this method to indicate + compliance. Alternatively, the object also has the option of + returning a wrapper object compliant with the protocol. Finally, + if the object cannot determine its compliance, it should either + return None or raise a TypeError to enable the remaining + mechanisms. + + To enable the third case, when the protocol knows about the + object, the protocol must have an __adapt__() method. This + optional method takes two arguments: + + - `self', the protocol requested + + - `obj', the object being adapted + + If the protocol finds the object to be compliant, it can return + obj directly. Alternatively, the method may return a wrapper + compliant with the protocol. Finally, compliance cannot be + determined, this method should either return None or raise a + TypeError so other mechanisms can be tried. + + The fourth case, when the object's class is a sub-class of the + protocol, is handled by the built-in adapt() function. Under + normal circumstances, if "isinstance(object, protocol)" then + adapt() returns the object directly. However, if the object is + not substitutable, either the __conform__() or __adapt__() methods + above may raise an adaptForceFailException to prevent this default + behavior. + + Please note two important things. First, this proposal does not + preclude the addition of other protocols. Second, this proposal + does not preclude other possible cases where adapter pattern may + hold, such as the context knowing the object and the protocol (the + last case in the requirements). In fact, this proposal opens the + gate for these other mechanisms to be added. + + +Reference Implementation and Test Cases + + ----------------------------------------------------------------- + adapt.py + ----------------------------------------------------------------- + import types + + adaptRaiseTypeException = "(raise a type exception on failure)" + adaptForceFailException = "(forced failure of adapt)" + + # look to see if the object passes other protocols + def _check(obj,protocol,default): + return default + + def adapt(obj, protocol, alternate = adaptRaiseTypeException): + + # first check to see if object has the exact protocol + if type(obj) is types.InstanceType and \ + obj.__class__ is protocol: return obj + if type(obj) is protocol: return obj + + # next check other protocols for exact conformance + # before calling __conform__ or __adapt__ + if _check(obj,protocol,0): + return obj + + # procedure to execute on success + def succeed(obj,retval,protocol,alternate): + if _check(retval,protocol,1): + return retval + else: + return fail(obj,alternate) + + # procedure to execute on failure + def fail(obj,protocol,alternate): + if alternate is adaptRaiseTypeException: + raise TypeError("%s cannot be adapted to %s" \ + % (obj,protocol)) + return alternate + + # try to use the object's adapting mechanism + conform = getattr(obj, '__conform__',None) + if conform: + try: + retval = conform(protocol) + if retval: + return succeed(obj,retval,protocol,alternate) + except adaptForceFailException: + return fail(obj,protocol,alternate) + except TypeError: pass + + # try to use the protocol's adapting mechanism + adapt = getattr(protocol, '__adapt__',None) + if adapt: + try: + retval = adapt(obj) + if retval: + return succeed(obj,retval,protocol,alternate) + except adaptForceFailException: + return fail(obj,protocol,alternate) + except TypeError: pass + + # check to see if the object is an instance + try: + if isinstance(obj,protocol): + return obj + except TypeError: pass + + # no-adaptation-possible case + return fail(obj,protocol,alternate) + + ----------------------------------------------------------------- + test.py + ----------------------------------------------------------------- + import types + from adapt import adaptForceFailException + from adapt import adapt + + class KnightsWhoSayNi: pass + + class Eggs: # an unrelated class/interface + def eggs(self): print "eggs!" + word = "Nee-womm" + + class Ham: # used as an interface, no inhertance + def ham(self): pass + word = "Ping" + + class Spam: # a base class, inheritance used + def spam(self): print "spam!" + + class EggsSpamAndHam (Spam,KnightsWhoSayNi): + def ham(self): print "ham!" + def __conform__(self,protocol): + if protocol is Ham: + # implements Ham's ham, but does not have a word + return self + if protocol is KnightsWhoSayNi: + # we are no longer the Knights who say Ni! + raise adaptForceFailException + if protocol is Eggs: + # Knows how to create the eggs! + return Eggs() + + class SacredWord: + class HasSecredWord: + def __call__(self, obj): + if getattr(obj,'word',None): return obj + __adapt__= HasSecredWord() + + class Bing (Ham): + def __conform__(self,protocol): + raise adaptForceFailException + + def test(): + x = EggsSpamAndHam() + adapt(x,Spam).spam() + adapt(x,Eggs).eggs() + adapt(x,Ham).ham() + adapt(x,EggsSpamAndHam).ham() + print adapt(Eggs(),SacredWord).word + print adapt(Ham(),SacredWord).word + pass + if adapt(x,KnightsWhoSayNi,None): raise "IckyIcky" + if not adapt(x,Spam,None): raise "Spam" + if not adapt(x,Eggs,None): raise "Eggs" + if not adapt(x,Ham,None): raise "Ham" + if not adapt(x,EggsSpamAndHam,None): raise "EggsAndSpam" + if adapt(x,KnightsWhoSayNi,None): raise "NightsWhoSayNi" + if adapt(x,SacredWord,None): raise "SacredWord" + try: + adapt(x,SacredWord) + except TypeError: pass + else: raise "SacredWord" + try: + adapt(x,KnightsWhoSayNi) + except TypeError: print "Ekky-ekky-ekky-ekky-z'Bang, " \ + + "zoom-Boing, z'nourrrwringmm" + else: raise "NightsWhoSayNi" + pass + b = Bing() + if not adapt(b,Bing,None): raise "Not a Bing" + if adapt(b,Ham,None): raise "Not a Ham!" + if adapt(1,types.FloatType,None): raise "Not a float!" + if adapt(b,types.FloatType,None): raise "Not a float!" + if adapt(1,Ham,None): raise "Not a Ham!" + if not adapt(1,types.IntType,None): raise "Is an Int!" + + ----------------------------------------------------------------- + Expected Output + ----------------------------------------------------------------- + >>> import test + >>> test.test() + spam! + eggs! + ham! + ham! + Nee-womm + Ping + Ekky-ekky-ekky-ekky-z'Bang, zoom-Boing, z'nourrrwringmm + >>> + + +Relationship To Paul Prescod and Tim Hochberg's Type Assertion method + + Paul and Tim had proposed a type checking mechanism, where the + Interface is passed an object to verify. The example syntax Paul + put forth recently [2] was: + + interface Interface + def __check__(self,obj) + + For discussion purposes, here would be a protocol with __check__: + + class Interface: + class Checker: + def __call__(self, obj): pass #check the object + __check__= Checker() + + The built-in adapt() function could be augmented to use this + checking mechanism updating the _check method as follows: + + # look to see if the object passes other protocols + def _check(obj,protocol,default): + check = getattr(protocol, '__check__',None) + if check: + try: + if check(obj): return 1 + except TypeError: pass + return 0 + else: + return default + + In short, the work put forth by Paul and company is great, and + there should be no problem preventing these two proposals from + working together in harmony, if not be completely complementary. + + +Relationship to Python Interfaces [1] by Michel Pelletier + + The relationship to this proposal to Michel's proposal could also + be complementary. Following is how the _check method would be + updated for this mechanism: + + # look to see if the object passes other protocols + def _check(obj,protocol,default): + if type(protocol) is types.InterfaceType: + return implements(obj,protocol) + return default + + +Relationship to Carlos Ribeiro's proxy technique [7] and [8] + + Carlos presented a technique where this method could return a + proxy instead of self or a wrapper. The advantage of this + approach is that the internal details of the object are protected. + This is very neat. No changes are necessary to this proposal to + support this usage as a standardized mechanism to obtain named + proxies. + + +Relationship To Microsoft's Query Interface + + Although this proposal may sounds similar to Microsoft's + QueryInterface, it differs by a number of aspects. + + First, it is bi-directional allowing the interface to be queried + as well giving more dynamic abilities (more Pythonic). Second, + there is not a special "IUnknown" interface which can be used for + object identity, although this could be proposed as one of those + "special" blessed interface protocol identifiers. Third, with + QueryInterface, once an object supports a particular interface it + must always there after support this interface; this proposal + makes no such guarantee, although this may be added at a later + time. Fourth, implementations of Microsoft's QueryInterface must + support a kind of equivalence relation. + + By reflexive they mean the querying an interface for itself must + always succeed. By symmetrical they mean that if one can + successfully query an interface IA for a second interface IB, then + one must also be able to successfully query the interface IB for + IA. And finally, by transitive they mean if one can successfully + query IA for IB and one can successfully query IB for IC, then one + must be able to successfully query IA for IC. Ability to support + this type of equivalence relation should be encouraged, but may + not be possible. Further research on this topic (by someone + familiar with Microsoft COM) would be helpful in further + determining how compatible this proposal is. + + +Question and Answer + + Q: What benefit does this provide? + + The typical Python programmer is an integrator, someone who is + connecting components from various vendors. Often times the + interfaces between these components require an intermediate + adapter. Usually the burden falls upon the programmer to + study the interface exposed by one component and required by + another, determine if they are directly compatible, or develop + an adapter. Sometimes a vendor may even include the + appropriate adapter, but then searching for the adapter and + figuring out how to deploy the adapter takes time. + + This technique enables vendors to work with each other + directly by implementing __conform__ or __adapt__ as + necessary. This frees the integrator from making their own + adapters. In essence, this allows the components to have a + simple dialogue among themselves. The integrator simply + connects one component to another, and if the types don't + automatically match an adapting mechanism is built-in. + + For example, consider SAX1 and SAX2 interfaces, there is an + adapter required to switch between them. Normally the + programmer must be aware of this; however, with this + adaptation framework this is no longer the case. + + Q: Why does this have to be built-in, can't it be standalone? + + Yes, it does work standalone. However, if it is built-in, it + has a greater chance of usage. The value of this proposal is + primarily in standardization. Furthermore: + + 0. The mechanism is by its very nature a singleton. + + 1. If used frequently, it will be much faster as a built-in + + 2. It is extensible and unassuming. + + 3. A whole-program optimizing compiler could optimize it out + in particular cases (ok, this one is far fetched) + + Q: Why the verbs __conform__ and __adapt__? + + conform, verb intransitive + 1. To correspond in form or character; be similar. + 2. To act or be in accord or agreement; comply. + 3. To act in accordance with current customs or modes. + + adapt, verb transitive + 1. To make suitable to or fit for a specific use or + situation. + + Source: The American Heritage Dictionary of the English + Language, Third Edition + + +Backwards Compatibility + + There should be no problem with backwards compatibility unless + someone had used __conform__ or __adapt__, but this seems + unlikely. Indeed this proposal, save an built-in adapt() + function, could be tested without changes to the interpreter. + + +Credits + + This proposal was created in large part by the feedback of the + talented individuals on both the main mailing list and also the + type-sig list. Specific contributors include (sorry if I missed + someone). + + This proposal is based largely off the suggestions from Alex + Martelli and Paul Prescod with significant feedback from Robin + Thomas and borrowing ideas from Marcin 'Qrczak' Kowalczyk and + Carlos Ribeiro. Other contributors (via comments) include Michel + Pelletier, Jeremy Hylton, Aahz Maruch, Fredrik Lundh, Rainer + Deyke, Timothy Delaney, and Huaiyu Zhu + + +References and Footnotes + + [1] PEP 245, Python Interface Syntax, Pelletier + http://www.python.org/peps/pep-0245.html + + [2] http://mail.python.org/pipermail/types-sig/2001-March/001223.html + + [3] http://www.zope.org/Members/michel/types-sig/TreasureTrove + + [4] http://mail.python.org/pipermail/types-sig/2001-March/001105.html + + [5] http://mail.python.org/pipermail/types-sig/2001-March/001206.html + + [6] http://mail.python.org/pipermail/types-sig/2001-March/001223.html + + [7] http://mail.python.org/pipermail/python-list/2001-March/035136.html + + [8] http://mail.python.org/pipermail/python-list/2001-March/035197.html + + [9] PEP 234, Iterators, Yee + http://www.python.org/peps/pep-0234.txt + + +Copyright + + This document has been placed in the public domain. + + + +Local Variables: +mode: indented-text +indent-tabs-mode: nil +End: