[Python-checkins] CVS: python/nondist/peps pep-0246.txt,NONE,1.1

Barry Warsaw bwarsaw@users.sourceforge.net
Wed, 28 Mar 2001 19:12:50 -0800


Update of /cvsroot/python/python/nondist/peps
In directory usw-pr-cvs1:/tmp/cvs-serv14537

Added Files:
	pep-0246.txt 
Log Message:
PEP 246, Object Adaptation, Clark C. Evans

(with editing for style, spell-checking, etc. by Barry)


--- NEW FILE: pep-0246.txt ---
PEP: 246
Title: Object Adaptation
Version: $Revision: 1.1 $
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: