[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