[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