11# -*- coding: utf-8; -*-
22"""A multiple-dispatch decorator for Python.
33
4- Somewhat like `functools.singledispatch`, but multiple dispatch.
4+ Somewhat like `functools.singledispatch`, but for multiple dispatch.
55
66 https://docs.python.org/3/library/functools.html#functools.singledispatch
77
1111
1212**WARNING: PROVISIONAL FEATURE**
1313
14- This provisional feature is provided for technical preview purposes only.
14+ This provisional feature is a proof-of-concept provided for technical preview
15+ and teaching purposes only.
16+
1517Details may still change in a backwards-incompatible way, or the whole
1618feature may still be removed. Do not depend on it in production!
1719"""
1820
19- from collections import defaultdict
21+ __all__ = ["generic" , "specific" ]
22+
2023from functools import wraps
2124import inspect
2225import typing
2326
2427from .arity import resolve_bindings
2528
26- registry = defaultdict (list )
27-
2829def generic (f ):
2930 """Decorator. Make `f` a generic function (in the sense of CLOS or Julia).
3031
31- Methods are attached by specifying type hints on parameters when defining
32- implementations. Then, at call time, types of **all** arguments are then
33- automatically used for choosing which method to call. Multiple parameters
34- may be used for dispatching.
32+ **How to use**:
33+
34+ The `@generic`-decorated function definition itself *is just a declaration*
35+ that defines the shape and parameter names of the formal parameter list.
36+ The formal parameter list must remain the same across all methods of the
37+ same generic function; only the types of the parameters may vary. That
38+ function is otherwise a stub. It is never called.
39+
40+ Like in `functools.singledispatch`, methods (implementations for specific
41+ combinations of argument types) are registered with the decorator
42+ `@f.register`, where `f` is the function you decorated with `@generic`.
43+
44+ Each method must specify type hints **on all parameters**. Then, at call
45+ time, types of **all** arguments are then automatically used for choosing
46+ which method to call, i.e., multiple parameters are used for dispatching.
47+
48+ The first match wins, in most-recently-registered order. So specify the
49+ implementation with the most generic types first, and then move on to the
50+ more specific ones. The mnemonic is, "the function is generally defined
51+ like this, except if the arguments match these particular types..."
3552
3653 The point of this feature is to eliminate `if`/`elif`/`elif`... blocks
37- that switch by `isinstance` on arguments (and then raise `TypeError`
38- in the final `else`), by implementing the machinery once centrally.
54+ that switch by `isinstance` on arguments, and then raise `TypeError`
55+ in the final `else`, by implementing the machinery once centrally.
56+
57+ **Differences to tools in the standard library**:
3958
40- Unlike `typing.overload`, the implementation belongs right there in the
41- type-specialized methods. Unlike `functools.singledispatch`, there is
42- no need to provide a fallback non-annotated implementation (and in fact
43- that is not supported).
59+ Unlike `functools.singledispatch`, the `@generic` function itself is
60+ unused, except as an interface declaration for the parameter list.
4461
45- Upon ambiguity, the method that was most recently registered wins. So
46- specify the implementation with the most generic types first, and then
47- move on to the more specific ones. The mnemonic is, "the function is
48- generally defined like this, except if you get arguments that match
49- these particular types..."
62+ Unlike `typing.overload`, the implementations are to be provided in the
63+ method bodies.
5064
5165 **CAUTION**:
5266
53- Currently, the shape of the parameter list must agree between all methods
54- of the same generic function, and advanced features of `typing` are not
55- supported .
67+ To declare a parameter of a method as dynamically typed, explicitly
68+ annotate it as `typing.Any`; don't just omit the type annotation.
69+ Explicit is better than implicit; this is a feature .
5670
57- Use `typing.Any` to declare a dynamically typed parameter - don't just omit
58- the type annotation, that won't work .
71+ Currently, advanced features of `typing` such as `Sequence[...]` are
72+ not supported. This may or may not change in the future .
5973 """
60- fullname = "{}.{}" .format (f .__module__ , f .__qualname__ )
61- signature = typing .get_type_hints (f )
62- registry [fullname ].append ((f , signature ))
74+ # Dispatcher - this will replace the original f.
6375 @wraps (f )
6476 def multidispatch (* args , ** kwargs ):
65- # TODO: This uses the definition of `f` this particular instance of the decorator
66- # TODO: was applied to. Maybe we should check, upon registration, that the shape
67- # TODO: of the parameter list matches for each method of the same generic function.
77+ # We use the parameter list of the original `@generic`-decorated
78+ # function to match the function call arguments to the formal
79+ # parameters of the function definition .
6880 bindings = resolve_bindings (f , * args , ** kwargs )
6981
7082 def match (signature ):
71- # TODO: handle *args (bindings["vararg"]) and **kwargs (bindings["kwarg"])
83+ # TODO: handle *args (bindings["vararg"])
84+ # TODO: handle **kwargs (bindings["kwarg"])
85+ # TODO: handle advanced features such as Sequence[int], Optional[str], ...
7286 for parameter , value in bindings ["args" ].items ():
7387 p = signature [parameter ] # TODO: what if parameter is not there? TypeError?
7488 if p is not typing .Any and not isinstance (value , p ):
7589 return False
7690 return True
7791
78- methods = tuple (reversed (registry [fullname ]))
79- for method , signature in methods :
92+ # Dispatch.
93+ def methods ():
94+ return reversed (multidispatch ._registry )
95+ for method , signature in methods ():
8096 if match (signature ):
8197 return method (* args , ** kwargs )
8298
@@ -86,7 +102,7 @@ def match(signature):
86102 # TODO: but in the general case this is difficult. We can't just `type(x)`, since
87103 # TODO: the signature may specify something like `Sequence[int]`. Knowing a `list`
88104 # TODO: was passed doesn't help debug that it was `Sequence[str]` when a `Sequence[int]`
89- # TODO: was expected. The actual value at least contains the type information implicitly .
105+ # TODO: was expected. The actual value at least implicitly contains the type information.
90106 #
91107 # TODO: Compute closest candidates, like Julia does? (see methods, MethodError)
92108 a = [str (a ) for a in args ]
@@ -97,31 +113,75 @@ def format_method(method): # Taking a page from Julia and some artistic liberty
97113 filename = inspect .getsourcefile (obj )
98114 source , firstlineno = inspect .getsourcelines (obj )
99115 return "{} from {}:{}" .format (signature , filename , firstlineno )
100- methods_str = [" {}" .format (format_method (x )) for x in methods ]
116+ methods_str = [" {}" .format (format_method (x )) for x in methods () ]
101117 candidates = "\n " .join (methods_str )
102118 msg = ("No method found matching {}({}{}{}).\n "
103119 "Candidate signatures (in order of match attempts):\n {}" ).format (f .__qualname__ ,
104120 ", " .join (a ),
105121 sep , ", " .join (kw ),
106122 candidates )
107123 raise TypeError (msg )
124+
125+ # fullname = "{}.{}".format(f.__module__, f.__qualname__)
126+ multidispatch ._registry = []
127+ def register (function ):
128+ """Decorator. Register a new method for this generic function.
129+
130+ The method must have type annotations for all of its parameters;
131+ these are used for dispatching.
132+ """
133+ # TODO: fail-fast: verify the shape of the parameter list of `function`
134+ # is compatible with the `@generic` function to which we are attaching
135+ # `function` as a new method.
136+ signature = typing .get_type_hints (function )
137+ multidispatch ._registry .append ((function , signature ))
138+ return multidispatch # Replace the function with the dispatcher for this generic function.
139+ multidispatch .register = register # publish the @f.register decorator
140+
108141 return multidispatch
109142
143+
144+ def specific (f ):
145+ """Decorator. A one-method pony, which is kind of the opposite of `@generic`.
146+
147+ This restricts the allowed argument types to one combination only. This can
148+ be used to eliminate `isinstance` boilerplate code in the function body, by
149+ allowing the types (for dynamic, run-time checking) to be specified with a
150+ very compact syntax.
151+
152+ Unlike in `@generic`, the function to be decorated simultaneously specifies
153+ both the shape and parameter names of the formal parameter list, as well as
154+ provides the body of the only method implementation.
155+
156+ A `@specific` function has no `.register` attribute; after it is created,
157+ no more methods can be attached to it.
158+ """
159+ s = generic (f )
160+ s .register (f )
161+ del s .register # remove the ability to attach more methods
162+ return s
163+
110164# --------------------------------------------------------------------------------
111165
112166@generic
167+ def zorblify (x , y ): # could use the ellipsis `...` as the body, but this is a unit test.
168+ assert False # Stub, used only for interface declaration. Never called.
169+ @zorblify .register
113170def zorblify (x : int , y : int ):
114171 return 2 * x + y
115- @generic
172+ @zorblify . register
116173def zorblify (x : str , y : int ):
117- # Because dispatching occurs on both arguments, this is not reached in tests.
118- assert False
119- @generic
174+ assert False # Because dispatching occurs on both arguments, this is not reached in tests.
175+ @zorblify .register
120176def zorblify (x : str , y : float ):
121177 return "{} {}" .format (x [::- 1 ], y )
122178
123179# TODO: def zorblify(x: int, *args: typing.Sequence[str]):
124180
181+ @specific
182+ def blubnify (x : int , y : float ):
183+ return x * y
184+
125185def test ():
126186 assert zorblify (17 , 8 ) == 42
127187 assert zorblify (17 , y = 8 ) == 42 # can also use named arguments
@@ -134,7 +194,16 @@ def test():
134194 except TypeError :
135195 pass
136196 else :
137- assert False # should have noticed there's no method registered for zorblify(float, float)
197+ assert False # there's no zorblify(float, float)
198+
199+ assert blubnify (2 , 21.0 ) == 42
200+ try :
201+ blubnify (2 , 3 )
202+ except TypeError :
203+ pass
204+ else :
205+ assert False # blubnify only accepts (int, float)
206+ assert not hasattr (blubnify , "register" )
138207
139208 print ("All tests PASSED" )
140209
0 commit comments