generic functions in python

Howard Stearns howard.stearns at charter.net
Sun Jun 6 20:53:05 EDT 2004


Howard Stearns wrote:
> Funny you should mention this today -- I was just sitting down to 
> implement generic functions myself. ...
> When I finish (who knows when?), I'll try to remember to post here and 
> cc you, Jim.
> ...
> Jim Newton wrote:
>> ...
>> perhaps there is a standard
>> mult-method-dispatch package available for use?

Try this. It's only 55 lines of code plus about that many in comments. Python's pretty cool, no?

"""
Generic Functions and Methods

 >>> foo = Generic_Function()
 >>> foo[object, object, object] = lambda _, x, y, z: 'default'
 >>> foo[int, int, int] = lambda call_next, x, y, z: ['all ints'] + [call_next(x, y, z)]
 >>> foo[object, object] = lambda _, x, y: 'just two'
 >>> foo(1, 2, 3)
['all ints', 'default']
 >>> foo(1, 2, 'three')
'default'
 >>> foo(1, 'two')
'just two'
 >>> foo('oops')
Traceback (most recent call last):
   ...
NoNextMethod: oops
"""
_AUTHOR=["Howard Stearns (stearns at alum.mit.edu)",]
_COPYRIGHT="""
Use this for anything you want, at your own risk.
Mistakes here are Howard's fault and no one elses.
copyright 2004
As a Python newbie, I do welcome comments on style & performance, as
well as on functionality.
"""
"""
There are several simplifications here compared with, say,
http://www.lisp.org/table/references.htm#mop
- FIXME: Currently, no support for **key args.
- No method or class metaobjects or mop.
- No support for method combination, or for before/after methods:
   But there is a call-next-method mechanism, so you can assemble your own
   results as you like.
- No real support for subclassing new kinds of generic functions with their
   own mechanisms.
And there is one extension:
+ Instead of dispatching only on the classes of a fixed number of positional
   arguments, dispatch is on both the number and type of arguments.

Class precence order is as defined by Python getmro().

David Mertz has a nice package at
http://www-106.ibm.com/developerworks/linux/library/l-pydisp.html
* This one differs in that there is only a single (powerful?) mechanism for
   method combination (call-next-method) instead of a built-in list option.
* Generic_Function() instances here are also dict objects, and you add/replace
   method by setting them using the signature as a key.
* This implementation builds a cache of effective methods instead of computing
   dispatches again for each call, so this is a lot faster.
"""

from UserDict import UserDict
from inspect import getmro

class NoNextMethod(Exception): pass

def raise_NoNextMethod(*args):
     raise NoNextMethod, args

class Generic_Function(UserDict, object):
     """A function that can have different method bodies separately defined.
     """
     def __init__(self):
         self.data = {} # All raw methods that have been defined.
         self.reset()
     def reset(self):
         self.cache = {}  # Combined methods that have actually been called.
     # Being a dict, my_func[typeA, typeB, ...] gives the method for those types.
     def __setitem__(self, signature, method_function):
         """Add or replace a method.

         e.g., my_func[Type1, Type2, ...] = lambda call_next, arg1, arg2, ...: body
         Within the body of the method, call_next is a function with the same
         signature as the whole generic function. It executes the next more general
         method that applies to the given arguments.
         """
         self.reset()  # Whenever we add a method, it invalidates the cache.
         UserDict.__setitem__(self, signature, method_function)
     def __call__(self, *dispatch_args):
         actual_types = tuple(map(type, dispatch_args))
         effective_method = self.cache.get(actual_types)
         if effective_method == None:
             effective_method = self.compute_effective_method(actual_types)
             self.cache[actual_types] = effective_method
         return effective_method(*dispatch_args)
     # The next two could reasonably be changed in subclasses to provide different behavior.
     def compute_effective_method(self, classes):
         """Uses the applicable method function to produce a single effective method.

         A method function takes a call_next argument. See __setitem___.
         An effective method has the same signature as the generic function, and is
         suitable as a call_next argument to a method function.
         """
         applicable_methods = self.compute_applicable_methods(classes)
         if applicable_methods == []:
             return raise_NoNextMethod
         else:
             return self.compute_effective_method_from_list(applicable_methods)
     def compute_applicable_methods(self, classes):
         pairs = []
         for signature, method in self.data.iteritems():
             if self.sig_applies_p(signature, classes):
                 pairs.append((signature, method))
         pairs.sort(lambda a, b: self.cmp_sigs_relative_to_arg_types(a[0], b[0], classes))
         methods = []
         return [pair[1] for pair in pairs]
     # Utilities ...
     def sig_applies_p(self, method_sig, arg_types):
         """True if each of method_sig is subclass of each of arg_types, for all of arg_types."""
         return reduce((lambda r, (a, b): r and (a!=None) and (b!=None) and issubclass(a, b)),
                       map(None, arg_types, method_sig),
                       True)
     def cmp_sigs_relative_to_arg_types(self, sig_a, sig_b, arg_types):
         """At the first place where sig_a and sig_b differ, which comes first
         in the full class precedence list of the corresponding type in arg_types."""
         as, bs = list(sig_a), list(sig_b)
         for arg_type in arg_types:
             a = as.pop(0)
             b = bs.pop(0)
             if a != b:
                 class_precedence_list = list(getmro(arg_type))
                 return class_precedence_list.index(a) - class_precedence_list.index(b)
     def compute_effective_method_from_list(self, method_functions):
         """Converts a sequence of supplied method functions to a single effective method function."""
         rest = method_functions[1:]
         if rest == []:
             call_next = raise_NoNextMethod
         else:
             more = self.compute_effective_method_from_list(rest)
             call_next = lambda *args: more(*args)
         return lambda *args: method_functions[0](call_next, *args)

def _test():
     import doctest, Generic_Function
     return doctest.testmod(Generic_Function)

if __name__ == "__main__":
     _test()






More information about the Python-list mailing list