diff options
| author | Michele Simionato <michele.simionato@gmail.com> | 2015-07-24 08:43:16 +0200 |
|---|---|---|
| committer | Michele Simionato <michele.simionato@gmail.com> | 2015-07-24 08:43:16 +0200 |
| commit | 55f28695e817fd7eac8870330843b3dff8f49eaa (patch) | |
| tree | aa941ca832e82153ae8499ec557c445e86e51058 /src | |
| parent | ae96a8f0844770bfb592012ac3ad06cdc7f8e3f9 (diff) | |
| download | python-decorator-git-55f28695e817fd7eac8870330843b3dff8f49eaa.tar.gz | |
Introduced dispatch_info
Diffstat (limited to 'src')
| -rw-r--r-- | src/decorator.py | 45 | ||||
| -rw-r--r-- | src/tests/documentation.py | 101 | ||||
| -rw-r--r-- | src/tests/test.py | 2 |
3 files changed, 102 insertions, 46 deletions
diff --git a/src/decorator.py b/src/decorator.py index 7e03d42..8faee1f 100644 --- a/src/decorator.py +++ b/src/decorator.py @@ -38,6 +38,7 @@ __version__ = '4.0.0' import re import sys import inspect +import operator import itertools import collections @@ -318,11 +319,11 @@ def dispatch_on(*dispatch_args): assert dispatch_args, 'No dispatch args passed' dispatch_str = '(%s,)' % ', '.join(dispatch_args) - def check(types): - """Make use one passes the expected number of types""" - if len(types) != len(dispatch_args): - raise TypeError('Expected %d types, got %d' % - (len(dispatch_args), len(types))) + def check(arguments, wrong=operator.ne, msg=''): + """Make sure one passes the expected number of arguments""" + if wrong(len(arguments), len(dispatch_args)): + raise TypeError('Expected %d arguments, got %d%s' % + (len(dispatch_args), len(arguments), msg)) def gen_func_dec(func): """Decorator turning a function into a generic function""" @@ -346,7 +347,7 @@ def dispatch_on(*dispatch_args): append(type_, ra) return [set(ra) for ra in ras] - def vmros(*types): + def ancestors(*types): """ Get a list of virtual MROs, one for each type """ @@ -362,25 +363,31 @@ def dispatch_on(*dispatch_args): mro = type('t', (t, va), {}).__mro__[1:] else: mro = t.__mro__ - lists.append(mro[:-1]) # discard object + lists.append(mro[:-1]) # discard t and object return lists def register(*types): - "Decorator to register an implementation for the given types" + """ + Decorator to register an implementation for the given types + """ check(types) - def dec(f): - n_args = len(getfullargspec(f).args) - if n_args < len(dispatch_args): - raise TypeError( - '%s has not enough arguments (got %d, expected %d)' % - (f, n_args, len(dispatch_args))) + check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__) typemap[types] = f return f return dec + def dispatch_info(*types): + """ + An utility to introspect the dispatch algorithm + """ + check(types) + lst = [] + for anc in itertools.product(*ancestors(*types)): + lst.append(tuple(a.__name__ for a in anc)) + return lst + def _dispatch(dispatch_args, *args, **kw): - "Dispatcher function" types = tuple(type(arg) for arg in dispatch_args) try: # fast path f = typemap[types] @@ -388,7 +395,9 @@ def dispatch_on(*dispatch_args): pass else: return f(*args, **kw) - for types_ in itertools.product(*vmros(*types)): + combinations = itertools.product(*ancestors(*types)) + next(combinations) # the first one has been already tried + for types_ in combinations: f = typemap.get(types_) if f is not None: return f(*args, **kw) @@ -399,8 +408,8 @@ def dispatch_on(*dispatch_args): return FunctionMaker.create( func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str, dict(_f_=_dispatch), register=register, default=func, - typemap=typemap, vancestors=vancestors, vmros=vmros, - __wrapped__=func) + typemap=typemap, vancestors=vancestors, ancestors=ancestors, + dispatch_info=dispatch_info, __wrapped__=func) gen_func_dec.__name__ = 'dispatch_on' + dispatch_str return gen_func_dec diff --git a/src/tests/documentation.py b/src/tests/documentation.py index 89dd6ec..04ee70e 100644 --- a/src/tests/documentation.py +++ b/src/tests/documentation.py @@ -700,18 +700,18 @@ to dispatch on more than one argument (for instance once I implemented a database-access library where the first dispatching argument was the the database driver and the second one was the database record), but here I prefer to follow the tradition and show the time-honored -Rock-Paper-Scissor example: +Rock-Paper-Scissors example: $$Rock $$Paper -$$Scissor +$$Scissors -I have added an ordinal to the Rock-Paper-Scissor classes to simplify +I have added an ordinal to the Rock-Paper-Scissors classes to simplify the implementation. The idea is to define a generic function ``win(a, b)`` of two arguments corresponding to the moves of the first and second player respectively. The moves are instances of the classes -Rock, Paper and Scissors; Paper wins over Rock, Scissor wins over -Paper and Rock wins over Scissor. The function will return +1 for a +Rock, Paper and Scissors; Paper wins over Rock, Scissors wins over +Paper and Rock wins over Scissors. The function will return +1 for a win, -1 for a loss and 0 for parity. There are 9 combinations, however combinations with the same ordinal (i.e. the same class) return 0; moreover by exchanging the order of the arguments the sign of the @@ -720,8 +720,8 @@ implementations: $$win $$winRockPaper -$$winPaperScissor -$$winRockScissor +$$winPaperScissors +$$winRockScissors Here is the result: @@ -729,23 +729,46 @@ Here is the result: >>> win(Paper(), Rock()) 1 - >>> win(Scissor(), Paper()) + >>> win(Scissors(), Paper()) 1 - >>> win(Rock(), Scissor()) + >>> win(Rock(), Scissors()) 1 >>> win(Paper(), Paper()) 0 >>> win(Rock(), Rock()) 0 - >>> win(Scissor(), Scissor()) + >>> win(Scissors(), Scissors()) 0 >>> win(Rock(), Paper()) -1 - >>> win(Paper(), Scissor()) + >>> win(Paper(), Scissors()) -1 - >>> win(Scissor(), Rock()) + >>> win(Scissors(), Rock()) -1 +The point of generic functions is that they play well with subclassing. +For instance, suppose we define a StrongRock which does not lose against +Paper: + +$$StrongRock +$$winStrongRockPaper + +Then we do not need to define other implementations, since they are +inherited from the parent: + +.. code-block:: python + + >>> win(StrongRock(), Scissors()) + 1 + +You can introspect the precedence used by the dispath algorithm by +calling ``.dispatch_info(*types)``: + +.. code-block:: python + + >>> win.dispatch_info(StrongRock, Scissors) + [('StrongRock', 'Scissors'), ('Rock', 'Scissors')] + Generic functions and virtual ancestors ------------------------------------------------- @@ -855,11 +878,24 @@ I will give an example showing the difference: $$singledispatch_example2 If you play with this example and replace the ``singledispatch`` definition -with ``functools.singledispatch``, you will see the output change from ``"s"`` -to ``"container"``, because ``functools.singledispatch`` +with ``functools.singledispatch``, the assert will break: ``g`` will return +``"container"`` instead of ``"s"``, because ``functools.singledispatch`` will insert the ``Container`` class right before ``S``. +The only way to understand what is happening here is to scratch your +head by looking at the implementations. I will just notice that +``.dispatch_info`` is quite useful: -Finally let me notice that the decorator module implementation does +.. code-block:: python + + >>> g, V = singledispatch_example2() + >>> g.dispatch_info(V) + [('V',), ('Sized',), ('S',), ('Container',)] + +The current implementation does not implement any kind of cooperation +between generic functions, i.e. there is nothing akin to call-next-method +in Lisp, nor akin to ``super`` in Python. + +Finally, let me notice that the decorator module implementation does not use any cache, whereas the one in ``singledispatch`` has a cache. Caveats and limitations @@ -1411,10 +1447,14 @@ class Paper(object): ordinal = 1 -class Scissor(object): +class Scissors(object): ordinal = 2 +class StrongRock(Rock): + pass + + @dispatch_on('a', 'b') def win(a, b): if a.ordinal == b.ordinal: @@ -1429,16 +1469,21 @@ def winRockPaper(a, b): return -1 -@win.register(Rock, Scissor) -def winRockScissor(a, b): +@win.register(Rock, Scissors) +def winRockScissors(a, b): return 1 -@win.register(Paper, Scissor) -def winPaperScissor(a, b): +@win.register(Paper, Scissors) +def winPaperScissors(a, b): return -1 +@win.register(StrongRock, Paper) +def winStrongRockPaper(a, b): + return 0 + + class WithLength(object): def __len__(self): return 0 @@ -1502,21 +1547,23 @@ def singledispatch_example2(): return 0 @singledispatch - def j(arg): + def g(arg): return "base" - @j.register(S) - def j_s(arg): + @g.register(S) + def g_s(arg): return "s" - @j.register(c.Container) - def j_container(arg): + @g.register(c.Container) + def g_container(arg): return "container" v = V() - assert j(v) == "s" + assert g(v) == "s" c.Container.register(V) # add c.Container to the virtual mro of V - return j(v) # "s", since the virtual mro is V, Sized, S, Container + assert g(v) == "s" # since the virtual mro is V, Sized, S, Container + return g, V + if __name__ == '__main__': import doctest diff --git a/src/tests/test.py b/src/tests/test.py index 1e1cb3f..951958c 100644 --- a/src/tests/test.py +++ b/src/tests/test.py @@ -36,7 +36,7 @@ class DocumentationTestCase(unittest.TestCase): def test_singledispatch2(self): if hasattr(functools, 'singledispatch'): - self.assertEqual(doc.singledispatch_example2(), "s") + doc.singledispatch_example2() class ExtraTestCase(unittest.TestCase): |
