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