python-peps/pep-0246.txt

548 lines
21 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
Post-History: 29-Mar-2001
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: