[Python-3000] Adaptation vs. Generic Functions

Guido van Rossum guido at python.org
Wed Apr 5 02:29:47 CEST 2006


#!/usr/bin/python2.4

"""An example of generic functions vs. adaptation.

After some face-to-face discussions with Alex I'm presenting here two
extremely simplified implementations of second-generation adaptation
and generic functions, in order to contrast and compare.

As the running example, we use the iterator protocol.  Pretend that
the iter() built-in function doesn't exist; how would we implement
iteration using adaptation or generic functions?

"""


__metaclass__ = type # Use new-style classes everywhere


# 0. The SequenceIter class is shared by all versions.

class SequenceIter:
    def __init__(self, obj):
        self.obj = obj
        self.index = 0
    def next(self):
        i = self.index
        self.index += 1
        if i < len(self.obj):
            return self.obj[i]
        raise StopIteration
    def __iter__(self):
        # This exists so we can use this in a for loop
        return self


# 1. Do it manually.  This is ugly but effective... until you need to
# iterate over a third party object type whose class you can't modify.

def ManualIter(obj):
    if isinstance(obj, (list, str, unicode)):
        return SequenceIter(obj)
    if isinstance(obj, dict):
        # We can't really do a better job without exposing PyDict_Next()
        return SequenceIter(obj.keys())
    if hasattr(obj, "__iter__"):
        return obj.__iter__()
    raise TypeError("Can't iterate over a %s object" % obj.__class__.__name__)


# 2. Using adaptation.  First I show a simple implementation of
# adaptation.  In a more realistic situation this would of course be
# imported.  I call this "second generation adaptation" because the
# adaptation from protocol P to type T is invoked as P.adapt(T), and
# the registration of an adapter function A for type T is invoked as
# P.register(T, A).  The only "smart" feature of this implementation
# is its support for inheritance (in T, not in P): if T has registered
# an adapter A for P, then A is also the default adapter for any
# subclass S of T, unless a more specific adapter is registered for S
# (or for some base of S that comes before T in S's MRO).

class Protocol:
    def __init__(self, name):
        self.registry = {}
        self.name = name
    def register(self, T, A):
        self.registry[T] = A
    def adapt(self, obj):
        for T in obj.__class__.__mro__:
            if T in self.registry:
                return self.registry[T](obj)
        raise TypeError("Can't adapt %s to %s" %
                        (obj.__class__.__name__, self.name))
    def __call__(self, T):
        # This is invoked when a Protocol instance is used as a decorator.
        def helper(A):
            self.register(T, A)
            return A
        return helper

# Now I show how to define and register the various adapters.  In a
# more realistic situation these don't all have to be in the same file
# of course.

AdaptingIterProtocol = Protocol("AdaptingIterProtocol")

@AdaptingIterProtocol(list)
def _AdaptingSequenceIter(obj):
    return SequenceIter(obj)

AdaptingIterProtocol.register(str, _AdaptingSequenceIter)
AdaptingIterProtocol.register(unicode, _AdaptingSequenceIter)

@AdaptingIterProtocol(dict)
def _AdaptingDictIter(obj):
    return SequenceIter(obj.keys())

@AdaptingIterProtocol(object)
def _AdaptingObjectIter(obj):
    if hasattr(obj, "__iter__"):
        return obj.__iter__()
    raise TypeError("Can't iterate over a %s object" % obj.__class__.__name__)

def AdaptingIter(obj):
    return AdaptingIterProtocol.adapt(obj)


# 3. Using generic functions.  First I show a simple implementation of
# generic functions.  In a more realistic situation this would of
# course be imported.

class GenericFunction:
    def __init__(self, default_function):
        self.default_function = default_function
        self.registry = {}
    def register(self, *args):
        def helper(F):
            self.registry[args] = F
            return F
        return helper
    def __call__(self, *args):
        types = tuple([obj.__class__ for obj in args])
        function = self.registry.get(types, self.default_function)
        return function(*args)

# Now I show how to define a generic function and how to register the
# various type-specific implementations.  In a more realistic
# situation these don't all have to be in the same file of course.

@GenericFunction
def GenericIter(obj):
    """This is the docstring for the generic function."""
    # The body is the default implementation
    if hasattr(obj, "__iter__"):
        return obj.__iter__()
    raise TypeError("Can't iterate over %s object" % obj.__class__.__name__)

@GenericIter.register(list)
def _GenericSequenceIter(obj):
    return SequenceIter(obj)

GenericIter.register(str)(_GenericSequenceIter)
GenericIter.register(unicode)(_GenericSequenceIter)

@GenericIter.register(dict)
def _GenericDictIter(obj):
    return SequenceIter(obj.keys())


# 4. Show that all of these work equivalently.

def main():
    examples = [
        [1, 2, 3, 4, 5],
        "abcde",
        u"ABCDE",
        {"x": 1, "y": 2, "z": 3},
        (6, 7, 8, 9, 10), # Not registered, but has __iter__ method
        42, # Not registered and has no __iter__ method
        ]

    functions = [ManualIter, AdaptingIter, GenericIter]

    for function in functions:
        print
        print "***", function, "***"
        for example in examples:
            print ":::", repr(example), ":::"
            try:
                iterator = function(example)
            except Exception, err:
                print "!!! %s: %s !!!" % (err.__class__.__name__, err)
            else:
                for value in function(example):
                    print repr(value),
                print

if __name__ == "__main__":
    main()

# --
# --Guido van Rossum (home page: http://www.python.org/~guido/)


More information about the Python-3000 mailing list