[Python-3000] Adaptation vs. Generic Functions

Nick Coghlan ncoghlan at gmail.com
Wed Apr 5 13:43:30 CEST 2006


Tim Hochberg wrote:
> So after all of that, I think my conclusion is that I wouldn't refactor 
> this at all, at least not yet. I'd add support for multiple registration 
> and possibly spell adapt as __call__, otherwise I'd leave it alone. My 
> opinion may change after I try ripping out keysof and see how it looks.

I was curious to see how the adaptation version actually looked with your and 
Guido's versions mixed. While writing it, I also noticed two interesting cases 
worth simplifying:
   1. the "no adapter needed case" (for registering that a type implements a 
protocol directly)
   2. the "missing adapter case" (for providing a default adaptation, as in 
the generic function case)

Here's what the whole thing ended up looking like:

def null_adapter(*args):
     """Adapter used when adaptation isn't actually needed"""
     if len(args) > 1:
         return args
     else:
         return args[0]

class Protocol(object):
     """Declare a named protocol"""
     def __init__(self, name):
         self.registry = {}
         self.name = name

     def register(self, adapter, *keys):
         """Register an adapter from given registry keys to the protocol"""
         if adapter is None:
             adapter = null_adapter
         for key in keys:
             self.registry[key] = adapter

     def register_for(self, *keys):
         """Function decorator to register as an adapter for given keys"""
         def helper(adapter):
             self.register(adapter, *keys)
             return adapter
         return helper

     def candidate_keys(self, call_args):
         """Find candidate registry keys for given call arguments"""
         # Default behaviour dispatches on the type of the first argument
         return type(call_args[0]).__mro__

     def default_adapter(self, *args):
         """Call result when no adapter was found"""
         raise TypeError("Can't adapt %s to %s" %
                         (args[0].__class__.__name__, self.name))

     def __call__(self, *args):
         """Adapt supplied arguments to this protocol"""
         for key in self.candidate_keys(args):
             try:
                 adapter = self.registry[key]
             except KeyError:
                 pass
             else:
                 return adapter(*args)
         return self.default_adapter(*args)


# The adapting iteration example
class AdaptingIterProtocol(Protocol):
     def __init__(self):
         Protocol.__init__(self, "AdaptingIter")

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

AdaptingIter = AdaptingIterProtocol()

AdaptingIter.register(SequenceIter, list, str, unicode)

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


# Building a generic function on top of that Protocol
class GenericFunction(Protocol):
     def __init__(self, default):
         Protocol.__init__(self, default.__name__)
         self.__doc__ = default.__doc__
         self.default_adapter = default

     def candidate_keys(self, call_args):
         """Find candidate registry keys for given call arguments"""
        arg_types = tuple(type(x) for x in call_args)
         if len(call_args) == 1:
             yield arg_types[0] # Allow bare type for single args
         yield arg_types # Always try full argument tuple

# The generic iteration example
@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())


-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://www.boredomandlaziness.org


More information about the Python-3000 mailing list