2014-08-21 17:20:57 +08:00
|
|
|
|
==============================
|
2014-09-02 04:46:28 +08:00
|
|
|
|
9.20 利用函数注解实现方法重载
|
2014-08-21 17:20:57 +08:00
|
|
|
|
==============================
|
|
|
|
|
|
|
|
|
|
|
|
----------
|
|
|
|
|
|
问题
|
|
|
|
|
|
----------
|
2015-04-27 17:28:33 +08:00
|
|
|
|
You’ve learned about function argument annotations and you have a thought that you
|
|
|
|
|
|
might be able to use them to implement multiple-dispatch (method overloading) based
|
|
|
|
|
|
on types. However, you’re not quite sure what’s involved (or if it’s even a good idea).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-08-21 17:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
----------
|
|
|
|
|
|
解决方案
|
|
|
|
|
|
----------
|
2015-04-27 17:28:33 +08:00
|
|
|
|
This recipe is based on a simple observation—namely, that since Python allows arguments
|
|
|
|
|
|
to be annotated, perhaps it might be possible to write code like this:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
class Spam:
|
|
|
|
|
|
def bar(self, x:int, y:int):
|
|
|
|
|
|
print('Bar 1:', x, y)
|
|
|
|
|
|
|
|
|
|
|
|
def bar(self, s:str, n:int = 0):
|
|
|
|
|
|
print('Bar 2:', s, n)
|
|
|
|
|
|
|
|
|
|
|
|
s = Spam()
|
|
|
|
|
|
s.bar(2, 3) # Prints Bar 1: 2 3
|
|
|
|
|
|
s.bar('hello') # Prints Bar 2: hello 0
|
|
|
|
|
|
|
|
|
|
|
|
Here is the start of a solution that does just that, using a combination of metaclasses and
|
|
|
|
|
|
descriptors:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
# multiple.py
|
|
|
|
|
|
import inspect
|
|
|
|
|
|
import types
|
|
|
|
|
|
|
|
|
|
|
|
class MultiMethod:
|
|
|
|
|
|
'''
|
|
|
|
|
|
Represents a single multimethod.
|
|
|
|
|
|
'''
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
|
|
self._methods = {}
|
|
|
|
|
|
self.__name__ = name
|
|
|
|
|
|
|
|
|
|
|
|
def register(self, meth):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Register a new method as a multimethod
|
|
|
|
|
|
'''
|
|
|
|
|
|
sig = inspect.signature(meth)
|
|
|
|
|
|
|
|
|
|
|
|
# Build a type signature from the method's annotations
|
|
|
|
|
|
types = []
|
|
|
|
|
|
for name, parm in sig.parameters.items():
|
|
|
|
|
|
if name == 'self':
|
|
|
|
|
|
continue
|
|
|
|
|
|
if parm.annotation is inspect.Parameter.empty:
|
|
|
|
|
|
raise TypeError(
|
|
|
|
|
|
'Argument {} must be annotated with a type'.format(name)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not isinstance(parm.annotation, type):
|
|
|
|
|
|
raise TypeError(
|
|
|
|
|
|
'Argument {} annotation must be a type'.format(name)
|
|
|
|
|
|
)
|
|
|
|
|
|
if parm.default is not inspect.Parameter.empty:
|
|
|
|
|
|
self._methods[tuple(types)] = meth
|
|
|
|
|
|
types.append(parm.annotation)
|
|
|
|
|
|
|
|
|
|
|
|
self._methods[tuple(types)] = meth
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, *args):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Call a method based on type signature of the arguments
|
|
|
|
|
|
'''
|
|
|
|
|
|
types = tuple(type(arg) for arg in args[1:])
|
|
|
|
|
|
meth = self._methods.get(types, None)
|
|
|
|
|
|
if meth:
|
|
|
|
|
|
return meth(*args)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise TypeError('No matching method for types {}'.format(types))
|
|
|
|
|
|
|
|
|
|
|
|
def __get__(self, instance, cls):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Descriptor method needed to make calls work in a class
|
|
|
|
|
|
'''
|
|
|
|
|
|
if instance is not None:
|
|
|
|
|
|
return types.MethodType(self, instance)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
class MultiDict(dict):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Special dictionary to build multimethods in a metaclass
|
|
|
|
|
|
'''
|
|
|
|
|
|
def __setitem__(self, key, value):
|
|
|
|
|
|
if key in self:
|
|
|
|
|
|
# If key already exists, it must be a multimethod or callable
|
|
|
|
|
|
current_value = self[key]
|
|
|
|
|
|
if isinstance(current_value, MultiMethod):
|
|
|
|
|
|
current_value.register(value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
mvalue = MultiMethod(key)
|
|
|
|
|
|
mvalue.register(current_value)
|
|
|
|
|
|
mvalue.register(value)
|
|
|
|
|
|
super().__setitem__(key, mvalue)
|
|
|
|
|
|
else:
|
|
|
|
|
|
super().__setitem__(key, value)
|
|
|
|
|
|
|
|
|
|
|
|
class MultipleMeta(type):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Metaclass that allows multiple dispatch of methods
|
|
|
|
|
|
'''
|
|
|
|
|
|
def __new__(cls, clsname, bases, clsdict):
|
|
|
|
|
|
return type.__new__(cls, clsname, bases, dict(clsdict))
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def __prepare__(cls, clsname, bases):
|
|
|
|
|
|
return MultiDict()
|
|
|
|
|
|
|
|
|
|
|
|
To use this class, you write code like this:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
class Spam(metaclass=MultipleMeta):
|
|
|
|
|
|
def bar(self, x:int, y:int):
|
|
|
|
|
|
print('Bar 1:', x, y)
|
|
|
|
|
|
|
|
|
|
|
|
def bar(self, s:str, n:int = 0):
|
|
|
|
|
|
print('Bar 2:', s, n)
|
|
|
|
|
|
|
|
|
|
|
|
# Example: overloaded __init__
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
class Date(metaclass=MultipleMeta):
|
|
|
|
|
|
def __init__(self, year: int, month:int, day:int):
|
|
|
|
|
|
self.year = year
|
|
|
|
|
|
self.month = month
|
|
|
|
|
|
self.day = day
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
t = time.localtime()
|
|
|
|
|
|
self.__init__(t.tm_year, t.tm_mon, t.tm_mday)
|
|
|
|
|
|
|
|
|
|
|
|
Here is an interactive session that verifies that it works:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
>>> s = Spam()
|
|
|
|
|
|
>>> s.bar(2, 3)
|
|
|
|
|
|
Bar 1: 2 3
|
|
|
|
|
|
>>> s.bar('hello')
|
|
|
|
|
|
Bar 2: hello 0
|
|
|
|
|
|
>>> s.bar('hello', 5)
|
|
|
|
|
|
Bar 2: hello 5
|
|
|
|
|
|
>>> s.bar(2, 'hello')
|
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
|
File "multiple.py", line 42, in __call__
|
|
|
|
|
|
raise TypeError('No matching method for types {}'.format(types))
|
|
|
|
|
|
TypeError: No matching method for types (<class 'int'>, <class 'str'>)
|
|
|
|
|
|
>>> # Overloaded __init__
|
|
|
|
|
|
>>> d = Date(2012, 12, 21)
|
|
|
|
|
|
>>> # Get today's date
|
|
|
|
|
|
>>> e = Date()
|
|
|
|
|
|
>>> e.year
|
|
|
|
|
|
2012
|
|
|
|
|
|
>>> e.month
|
|
|
|
|
|
12
|
|
|
|
|
|
>>> e.day
|
|
|
|
|
|
3
|
|
|
|
|
|
>>>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-08-21 17:20:57 +08:00
|
|
|
|
|
|
|
|
|
|
----------
|
|
|
|
|
|
讨论
|
|
|
|
|
|
----------
|
2015-04-27 17:28:33 +08:00
|
|
|
|
Honestly, there might be too much magic going on in this recipe to make it applicable
|
|
|
|
|
|
to real-world code. However, it does dive into some of the inner workings of metaclasses
|
|
|
|
|
|
and descriptors, and reinforces some of their concepts. Thus, even though you might
|
|
|
|
|
|
not apply this recipe directly, some of its underlying ideas might influence other programming
|
|
|
|
|
|
techniques involving metaclasses, descriptors, and function annotations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The main idea in the implementation is relatively simple. The MutipleMeta metaclass
|
|
|
|
|
|
uses its __prepare__() method to supply a custom class dictionary as an instance of
|
|
|
|
|
|
MultiDict. Unlike a normal dictionary, MultiDict checks to see whether entries already
|
|
|
|
|
|
exist when items are set. If so, the duplicate entries get merged together inside an instance
|
|
|
|
|
|
of MultiMethod.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Instances of MultiMethod collect methods by building a mapping from type signatures
|
|
|
|
|
|
to functions. During construction, function annotations are used to collect these signatures
|
|
|
|
|
|
and build the mapping. This takes place in the MultiMethod.register()
|
|
|
|
|
|
method. One critical part of this mapping is that for multimethods, types must be
|
|
|
|
|
|
specified on all of the arguments or else an error occurs.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
To make MultiMethod instances emulate a callable, the __call__() method is implemented.
|
|
|
|
|
|
This method builds a type tuple from all of the arguments except self, looks
|
|
|
|
|
|
up the method in the internal map, and invokes the appropriate method. The __get__()
|
|
|
|
|
|
is required to make MultiMethod instances operate correctly inside class definitions. In
|
|
|
|
|
|
the implementation, it’s being used to create proper bound methods. For example:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
>>> b = s.bar
|
|
|
|
|
|
>>> b
|
|
|
|
|
|
<bound method Spam.bar of <__main__.Spam object at 0x1006a46d0>>
|
|
|
|
|
|
>>> b.__self__
|
|
|
|
|
|
<__main__.Spam object at 0x1006a46d0>
|
|
|
|
|
|
>>> b.__func__
|
|
|
|
|
|
<__main__.MultiMethod object at 0x1006a4d50>
|
|
|
|
|
|
>>> b(2, 3)
|
|
|
|
|
|
Bar 1: 2 3
|
|
|
|
|
|
>>> b('hello')
|
|
|
|
|
|
Bar 2: hello 0
|
|
|
|
|
|
>>>
|
|
|
|
|
|
|
|
|
|
|
|
To be sure, there are a lot of moving parts to this recipe. However, it’s all a little unfortunate
|
|
|
|
|
|
considering how many limitations there are. For one, the solution doesn’t work
|
|
|
|
|
|
with keyword arguments: For example:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
>>> s.bar(x=2, y=3)
|
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
|
TypeError: __call__() got an unexpected keyword argument 'y'
|
|
|
|
|
|
|
|
|
|
|
|
>>> s.bar(s='hello')
|
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
|
TypeError: __call__() got an unexpected keyword argument 's'
|
|
|
|
|
|
>>>
|
|
|
|
|
|
|
|
|
|
|
|
There might be some way to add such support, but it would require a completely different
|
|
|
|
|
|
approach to method mapping. The problem is that the keyword arguments don’t
|
|
|
|
|
|
arrive in any kind of particular order. When mixed up with positional arguments, you
|
|
|
|
|
|
simply get a jumbled mess of arguments that you have to somehow sort out in the
|
|
|
|
|
|
__call__() method.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This recipe is also severely limited in its support for inheritance. For example, something
|
|
|
|
|
|
like this doesn’t work:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
class A:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class B(A):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class C:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Spam(metaclass=MultipleMeta):
|
|
|
|
|
|
def foo(self, x:A):
|
|
|
|
|
|
print('Foo 1:', x)
|
|
|
|
|
|
|
|
|
|
|
|
def foo(self, x:C):
|
|
|
|
|
|
print('Foo 2:', x)
|
|
|
|
|
|
|
|
|
|
|
|
The reason it fails is that the x:A annotation fails to match instances that are subclasses
|
|
|
|
|
|
(such as instances of B). For example:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
>>> s = Spam()
|
|
|
|
|
|
>>> a = A()
|
|
|
|
|
|
>>> s.foo(a)
|
|
|
|
|
|
Foo 1: <__main__.A object at 0x1006a5310>
|
|
|
|
|
|
>>> c = C()
|
|
|
|
|
|
>>> s.foo(c)
|
|
|
|
|
|
Foo 2: <__main__.C object at 0x1007a1910>
|
|
|
|
|
|
>>> b = B()
|
|
|
|
|
|
>>> s.foo(b)
|
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
|
File "multiple.py", line 44, in __call__
|
|
|
|
|
|
raise TypeError('No matching method for types {}'.format(types))
|
|
|
|
|
|
TypeError: No matching method for types (<class '__main__.B'>,)
|
|
|
|
|
|
>>>
|
|
|
|
|
|
|
|
|
|
|
|
As an alternative to using metaclasses and annotations, it is possible to implement a
|
|
|
|
|
|
similar recipe using decorators. For example:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
import types
|
|
|
|
|
|
|
|
|
|
|
|
class multimethod:
|
|
|
|
|
|
def __init__(self, func):
|
|
|
|
|
|
self._methods = {}
|
|
|
|
|
|
self.__name__ = func.__name__
|
|
|
|
|
|
self._default = func
|
|
|
|
|
|
|
|
|
|
|
|
def match(self, *types):
|
|
|
|
|
|
def register(func):
|
|
|
|
|
|
ndefaults = len(func.__defaults__) if func.__defaults__ else 0
|
|
|
|
|
|
for n in range(ndefaults+1):
|
|
|
|
|
|
self._methods[types[:len(types) - n]] = func
|
|
|
|
|
|
return self
|
|
|
|
|
|
return register
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, *args):
|
|
|
|
|
|
types = tuple(type(arg) for arg in args[1:])
|
|
|
|
|
|
meth = self._methods.get(types, None)
|
|
|
|
|
|
if meth:
|
|
|
|
|
|
return meth(*args)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return self._default(*args)
|
|
|
|
|
|
|
|
|
|
|
|
def __get__(self, instance, cls):
|
|
|
|
|
|
if instance is not None:
|
|
|
|
|
|
return types.MethodType(self, instance)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
To use the decorator version, you would write code like this:
|
|
|
|
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
|
|
|
|
class Spam:
|
|
|
|
|
|
@multimethod
|
|
|
|
|
|
def bar(self, *args):
|
|
|
|
|
|
# Default method called if no match
|
|
|
|
|
|
raise TypeError('No matching method for bar')
|
|
|
|
|
|
|
|
|
|
|
|
@bar.match(int, int)
|
|
|
|
|
|
def bar(self, x, y):
|
|
|
|
|
|
print('Bar 1:', x, y)
|
|
|
|
|
|
|
|
|
|
|
|
@bar.match(str, int)
|
|
|
|
|
|
def bar(self, s, n = 0):
|
|
|
|
|
|
print('Bar 2:', s, n)
|
|
|
|
|
|
|
|
|
|
|
|
The decorator solution also suffers the same limitations as the previous implementation
|
|
|
|
|
|
(namely, no support for keyword arguments and broken inheritance).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All things being equal, it’s probably best to stay away from multiple dispatch in generalpurpose
|
|
|
|
|
|
code. There are special situations where it might make sense, such as in programs
|
|
|
|
|
|
that are dispatching methods based on some kind of pattern matching. For example,
|
|
|
|
|
|
perhaps the visitor pattern described in Recipe 8.21 could be recast into a class
|
|
|
|
|
|
that used multiple dispatch in some way. However, other than that, it’s usually never a
|
|
|
|
|
|
bad idea to stick with a more simple approach (simply use methods with different
|
|
|
|
|
|
names).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Ideas concerning different ways to implement multiple dispatch have floated around
|
|
|
|
|
|
the Python community for years. As a decent starting point for that discussion, see
|
|
|
|
|
|
Guido van Rossum’s blog post
|
|
|
|
|
|
`Five-Minute Multimethods in Python <http://www.artima.com/weblogs/viewpost.jsp?thread=101605>`_
|
|
|
|
|
|
|