[Types-sig] QueryProtocol
Clark C. Evans
cce@clarkevans.com
Wed, 21 Mar 2001 04:30:16 -0500 (EST)
Paul and Michel,
Thank you both very much for your thoughtful feedback.
I've made significant changes, many based on your
comments. I'd very much apprechiate further consideration.
Thank you! ;) Clark
P.S. I am particularly hesitant about the baseclass matching
mechanism, however, when you move to classes as an
identifier, I don't think much choice exists. Yes?
---------- Forwarded message ----------
Date: Wed, 21 Mar 2001 04:23:29 -0500
From: Clark C. Evans <cce@clarkevans.com>
To: python-list@python.org
Newsgroups: comp.lang.python
Subject: Yet Another PEP: Interface Adapter Mechanism __adapt__
Thank you all for your feedback and support with the first
pass of this PEP. Below is a second attempt. The context and
motivation sections are identical. Here are the major changes:
* __query__ is renamed __adapt__ (adapter pattern)
* __adapt__ now takes a class instance (instead of a DNS string)
* __adapt__ now returns None if the lookup was unsuccessful
* straw-man adapt function is detailed
* the title of this PEP is updated respectively
Regards,
Clark Evans
PEP: XXX
Title: Interface Adapter Mechanism
Version: $Revision$
Author: Clark Evans
Python-Version: 2.2
Status: Draft
Type: Standards Track
Created: 21-Mar-2001
Updated: 22-Mar-2001
Summary
This paper asserts that "interface typing" can be carved
into two separable concerns, that of (a) protocol, which is
all about behavior/expectations, and that of (b) signature
which is about method existence and argument type checking.
This proposal puts forth a declarative method for interface
protocol discovery that could be orthogonal and complementary
to a more signature based approach as being developed by the
types special interest group.
The proposal is modeled after the adapter pattern as described
design patterns book by Gamma, Helm, Johnson and Vlissides.
Context
Python is a very dynamic language with powerful introspection
capabilities. However, it has yet to formalize an interface
or abstract type checking mechanism. A consensus for a user
defined type system may be far off into the future.
Currently, existence of particular methods, particularly those
that are built-in such as __getitem__, is used as an indicator
of support for a particular interface. This method may work
for interfaces blessed by GvR, such as the new enumerator interface
being proposed and identified by a new built-in __iter__.
However, this current method does not admit an infallible way
to identify interfaces lacking a built-in method.
Motivation
In the recent type special interest group discussion [1], there
were two complementary quotes which motivated this proposal:
"The deep(er) part is whether the object passed in thinks of
itself as implementing the Foo interface. This means that
its author has (presumably) spent at least a little time
about the invariants that a Foo should obey." GvR [2]
and
"There is no concept of asking an object which interface it
implements. There is no "the" interface it implements. It's
not even a set of interfaces, because the object doesn't
know them in advance. Interfaces can be defined after objects
conforming to them are created." -- Marcin Kowalczyk [3]
The first quote focuses on the intent of a class, including
not only the existence of particular methods, but more
importantly the call sequence, behavior, and other invariants.
Where the second quote focuses on the type signature of the
class. These quotes motivate a distinction between interface
as a "declarative, I am a such-and-such" construct, as opposed
to a "descriptive, It looks like a such-and-such" mechanism.
Furthermore, it is clear that both aspects of interface are
important, if not completely orthogonal and complementary.
For purposes of this proposal, the word "protocol" is aligned
with Guido's deep issue, and "signature" with Marcin's vision
of a type system. In this way, we clearly demarcate the two
attitudes of the word "interface" in a type system.
Clearly, detailing an exhaustive method of interface
signatures is very difficult problem, especially for
a dynamic language like Python where the interface
signature for a class may change over time. However, a
simple declarative mechanism to inquire which interface
protocols an object supports at a given time is relatively
straight forward and could bring immediate benefit.
Details
A new built-in method, __adapt__ is proposed. This method has
a single argument, an adapter identifier, and either returns
an object supporting the given protocol or it returns None.
As of this PEP, an adapter identifier may only be a class object.
Future PEPs may expand the scope to allow other types of objects
and will detail how they are treated.
Further. a built-in function, adapt is suggested. This function
takes two arguments, an object and an identifier. Initially, this
function calls the __adapt__ method of the object, passing in
the identifier. If the __adapt__ method provides an object, then
this is returned. Otherwise, if the __adapt__ method is not found,
or if the __adapt__ method returns None, then the objects class
ancestry is checked to see if the identifier is a member. If so,
then the object itself is returned. Otherwise, if no other PEPs
apply, then None is return value of the adapt function. Following
is a sample implementation of this built-in.
def adapt(obj,ident, options = None):
global check_base
def check_base(bas,cmp):
if bas == cmp: return 1
for base in bas.__bases__:
if check_base(base,cmp): return 1
return 0
if hasattr(obj, '__adapt__'):
retval = obj.__adapt__(ident)
if retval == None:
if check_base(obj.__class__,ident):
retval = obj
# options flag used to enable:
# - reverse lookup via ident (future PEP) goes here?
# - signature based lookup (future PEP) goes here?
# - automatic signature checking (future PEP) goes here?
return retval
Example Usage
>>> class KnightsWhoSayNi: pass
>>> class EggsOnly: # an unrelated class/interface
def eggs(self,str): print "eggs!" + str
>>> class HamOnly: # used as an interface, no inhertance
def ham(self,str): pass
def _bugger(self): pass # irritating a private member
>>> class SpamOnly: # a base class, inheritance used
def spam(self,str): print "spam!" + str
>>> class EggsSpamAndHam (SpamOnly):
def ham(self,str): print "ham!" + str
def __adapt__(self,cls):
if cls == HamOnly:
return self # HamOnly implicit, no _bugger
if cls == EggsOnly:
return EggsOnly() # Knows how to create the eggs!
return None
>>> import adapter.example
>>> from adapter import adapt
>>> x = adapter.example.EggsSpamAndHam()
>>> adapt(x,adapter.example.SpamOnly).spam("Ni!")
spam!Ni!
>>> adapt(x,adapter.example.EggsOnly).eggs("Ni!")
eggs!Ni!
>>> adapt(x,adapter.example.HamOnly).ham("Ni!")
ham!Ni!
>>> adapt(x,adapter.example.EggsSpamAndHam).ham("Ni!")
ham!Ni!
>>> adapt(x,adapter.example.KnightsWhoSayNi).spam("Ni!")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
AttributeError: 'None' object has no attribute 'spam'
Relationships To Iterator Proposal:
The iterator special interest group is proposing a new built-in
called "__iter__", which could be replaced with __adapt__ if an
an Interator class is introduced. Following is an example.
>>> from adapter import adapt
>>> import adapter.example
>>> x = adapter.example.IteratorTest(3)
>>> iter = adapt(x,adapter.example.Iterator)
>>> iter.next()
1
>>> iter.next()
2
>>> iter.next()
Traceback (innermost last):
File "<interactive input>", line 1, in ?
File "c:\work\adapter\example.py", line 39, in next
return Iterator.next(self)
File "c:\work\adapter\example.py", line 24, in next
raise IndexError
IndexError:
Relationships To Microsofts Query Interface:
Although this proposal may sounds similar to Microsofts
QueryInterface, it differs by a number of aspects. First,
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.
Second, 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. Third, implementations of Microsofts
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.
Backwards Compatibility
There should be no problem with backwards compatibility.
Indeed this proposal, save an built-in adapt() function,
could be tested without changes to the interpreter.
Future Compatibility
It appears that this proposal could be implemented orthogonal
to the protocol checking system being constructed [4] by Paul
Prescod and company. In particular, a new PEP could be added
which allows for Interface objects to be used as the adapter
identifier. Then, the __check__ method could be used within
the adapt() method.
It is less clear to me how this proposal would work with the
the more ambitious signature declaration and checking approach[5]
taken by Michel Pelletier. This requires further investigation.
Questions and Answers
Q: Why was the name changed from __query__ to __adapt__ ?
A: It was clear that significant QueryInterface assumptions were
being laid upon the proposal, when the intent was more of an
adapter. Of course, if an object does not need to be adapted
then it can be used directly and this is the basic premise.
Q: This is just a type-coercion proposal.
A: No. Certainly it could be used for type-coercion, such
coercion would be explicit via __adapt__ or adapt function.
Of course, if this was used for iterator interface, then the
for construct may do an implicit __adapt__(Iterator) but
this would be an exception rather than the rule.
Q: Why did the author write this PEP?
A: He wanted a simple proposal that covered the "deep part" of
interfaces without getting tied up in signature woes. Also, it
was clear that __iter__ proposal put forth is just an example
of this type of interface. Further, the author is doing XML
based client server work, and wants to write generic tree based
algorithms that work on particular interfaces and would
like these algorithms to be used by anyone willing to make
an "adapter" having the interface required by the algorithm.
Q: Why not call this __queryinterface__ ?
A: Too close to Microsofts QueryInterface, especially given the
semantic differences which may not be reconcilable.
Q: Is this in opposition to the type special interest group?
A: No. It is meant as a simple, need based solution that could
easily complement the efforts by that group.
Q: Why was the identifier changed from a string to a class?
A: This was done on Michel Pelletiers suggestion. This mechanism
appears to be much cleaner than the DNS string proposal, which
caused a few eyebrows to rise.
Q: Why not allow any object to be an identifier, why just classes?
A: It would be hard to get forward compatibility with new behaviors
described in other PEPs.
Q: It seems that a reverse lookup could be used, why not add this?
A: There are many other lookup and/or checking mechanisms that
could be used here. However, the goal of this PEP is to be
small and sweet ... having any more functionality would make
it more objectionable to some people. However, this proposal
was designed in large part to be completely orthogonal to other
methods, so these mechanisms can be added later if needed
Copyright
This document has been placed in the public domain.
References and Footnotes
[1] http://www.zope.org/Members/michel/types-sig/TreasureTrove
[2] http://mail.python.org/pipermail/types-sig/2001-March/001105.html
[3] http://mail.python.org/pipermail/types-sig/2001-March/001206.html
[4] http://mail.python.org/pipermail/types-sig/2001-March/001223.html
-----------------------------------------------------
adapter/__init__.py
-----------------------------------------------------
def adapt(obj,cls, options = None):
global check_base
def check_base(bas,cmp):
if bas == cmp: return 1
for base in bas.__bases__:
if check_base(base,cmp): return 1
return 0
if hasattr(obj, '__adapt__'):
retval = obj.__adapt__(cls)
if retval == None:
if check_base(obj.__class__,cls):
retval = obj
# options flag used to enable:
# - reverse lookup via cls (future PEP) goes here?
# - signature based lookup (future PEP) goes here?
# - automatic signature checking (future PEP) goes here?
return retval
------------------------------------------------------
adapter/example.py
------------------------------------------------------
class EggsOnly: # an unrelated class/interface
def eggs(self,str): print "eggs!" + str
class HamOnly: # used as an interface, no inhertance
def ham(self,str): pass
def _bugger(self): pass # irritating a private member
class SpamOnly: # a base class, inheritance used
def spam(self,str): print "spam!" + str
class Intermediate (SpamOnly): pass
class EggsSpamAndHam (Intermediate):
def ham(self,str): print "ham!" + str
def __adapt__(self,cls):
if cls == HamOnly:
return self # implements HamOnly implicitly, no _bugger
if cls == EggsOnly:
return EggsOnly() # Knows how to create the eggs!
return None
class Iterator:
def next(self):
raise IndexError
class IteratorTest:
def __init__(self,max):
self.max = max
def __adapt__(self,cls):
if cls == Iterator:
class IteratorTestIterator(Iterator):
def __init__(self,max):
self.max = max
self.count = 0
def next(self):
self.count = self.count + 1
if self.count < self.max:
return self.count
return Iterator.next(self)
return IteratorTestIterator(self.max)
return None