W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
你已經(jīng)學(xué)過怎樣使用函數(shù)參數(shù)注解,那么你可能會想利用它來實現(xiàn)基于類型的方法重載。但是你不確定應(yīng)該怎樣去實現(xiàn)(或者到底行得通不)。
本小節(jié)的技術(shù)是基于一個簡單的技術(shù),那就是Python允許參數(shù)注解,代碼可以像下面這樣寫:
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
下面是我們第一步的嘗試,使用到了一個元類和描述器:
# 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()
為了使用這個類,你可以像下面這樣寫:
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)
下面是一個交互示例來驗證它能正確的工作:
>>> 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
>>>
坦白來講,相對于通常的代碼而已本節(jié)使用到了很多的魔法代碼。但是,它卻能讓我們深入理解元類和描述器的底層工作原理,并能加深對這些概念的印象。因此,就算你并不會立即去應(yīng)用本節(jié)的技術(shù),它的一些底層思想?yún)s會影響到其它涉及到元類、描述器和函數(shù)注解的編程技術(shù)。
本節(jié)的實現(xiàn)中的主要思路其實是很簡單的。MutipleMeta
元類使用它的 __prepare__()
方法來提供一個作為 MultiDict
實例的自定義字典。這個跟普通字典不一樣的是,MultiDict
會在元素被設(shè)置的時候檢查是否已經(jīng)存在,如果存在的話,重復(fù)的元素會在 MultiMethod
實例中合并。
MultiMethod
實例通過構(gòu)建從類型簽名到函數(shù)的映射來收集方法。在這個構(gòu)建過程中,函數(shù)注解被用來收集這些簽名然后構(gòu)建這個映射。這個過程在 MultiMethod.register()
方法中實現(xiàn)。這種映射的一個關(guān)鍵特點是對于多個方法,所有參數(shù)類型都必須要指定,否則就會報錯。
為了讓 MultiMethod
實例模擬一個調(diào)用,它的 __call__()
方法被實現(xiàn)了。這個方法從所有排除 slef
的參數(shù)中構(gòu)建一個類型元組,在內(nèi)部map中查找這個方法,然后調(diào)用相應(yīng)的方法。為了能讓 MultiMethod
實例在類定義時正確操作,__get__()
是必須得實現(xiàn)的。它被用來構(gòu)建正確的綁定方法。比如:
>>> 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
>>>
不過本節(jié)的實現(xiàn)還有一些限制,其中一個是它不能使用關(guān)鍵字參數(shù)。例如:
>>> 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'
>>>
也許有其他的方法能添加這種支持,但是它需要一個完全不同的方法映射方式。問題在于關(guān)鍵字參數(shù)的出現(xiàn)是沒有順序的。當它跟位置參數(shù)混合使用時,那你的參數(shù)就會變得比較混亂了,這時候你不得不在 __call__()
方法中先去做個排序。
同樣對于繼承也是有限制的,例如,類似下面這種代碼就不能正常工作:
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)
原因是因為 x:A
注解不能成功匹配子類實例(比如B的實例),如下:
>>> 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'>,)
>>>
作為使用元類和注解的一種替代方案,可以通過描述器來實現(xiàn)類似的效果。例如:
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
為了使用描述器版本,你需要像下面這樣寫:
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)
描述器方案同樣也有前面提到的限制(不支持關(guān)鍵字參數(shù)和繼承)。
所有事物都是平等的,有好有壞,也許最好的辦法就是在普通代碼中避免使用方法重載。不過有些特殊情況下還是有意義的,比如基于模式匹配的方法重載程序中。舉個例子,8.21小節(jié)中的訪問者模式可以修改為一個使用方法重載的類。但是,除了這個以外,通常不應(yīng)該使用方法重載(就簡單的使用不同名稱的方法就行了)。
在Python社區(qū)對于實現(xiàn)方法重載的討論已經(jīng)由來已久。對于引發(fā)這個爭論的原因,可以參考下Guido van Rossum的這篇博客:Five-Minute Multimethods in Python
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: