[Python-3000] Adaptation vs. Generic Functions

Tim Hochberg tim.hochberg at ieee.org
Wed Apr 5 15:55:26 CEST 2006


Nick Coghlan wrote:
> 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)

I like this version. The naming seems an improvement and the 
default_adapter seems like a worthy addition. I do keep wondering if 
there's some reason for a user of Protocol to call candidate_keys 
directly or it's only an implementation detail. If the there is such a 
reason, and we could figure it out, it would probably immediately 
obvious what to call it. If there isn't, perhaps it should be prefixed 
with '_' to indicate that it's not part of the public interface.


> # 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())
> 

These should all be "GenericIter.register_for", right?

Regards,

-tim



More information about the Python-3000 mailing list