-- TOC --
Python标准的functools
模块中,提供了一个函数装饰器 @singledispatch
,可以用来实现单参数函数接口重载
。所谓单参数重载,是指只对函数接口的第1个参数进行泛型化重载处理,其它参数的不同类型不会影响最终被调用的接口,Python解释器在选择最终调用的接口时,会直接忽略非第1个参数。
C++的函数重载,与函数的整个入参列表都相关。而Python的单参数重载,只与第1个参数的类型有关系。
虽然像python这样的动态类型语言,函数重载这样的概念是天生内嵌在其设计理念中的,任何Python函数接口的参数,运行时都无所谓什么类型,如果某个参数需要多类型,内部代码可自行判断处理。但Python官方团队还是搞出来这么个@singledispatch装饰器,它能够在一定程度上简化代码,并优化可读性。anyway,我们来学习一下它的用法,自己不用,也许在别人的代码中能够读到呢。。
重载(overload)和泛型(generic type)有些不一样,后者是直接用type进行泛化编程,有点metaprogramming的意思...
直接上代码:
from functools import singledispatch
@singledispatch
def connect(address):
print(address)
@connect.register
def _(addr: str):
ip, port = addr.split(':')
print(f'IP:{ip}, port:{port}')
@connect.register
def _(addr: tuple):
ip, port = addr
print(f'IP:{ip}, port:{port}')
connect('123.45.67.18:12345')
connect(('123.45.32.18', 23456))
connect(567)
用 @singledispatch
装饰主函数connect,主函数很简单,直接打印入参。然后用衍生的@connect.register
装饰主函数不同首参数类型的版本,此时函数名已无关紧要。执行时,3次调用connect,分别传输str,tuple和int,执行效果如下:
IP:123.45.67.18, port:12345 # str version
IP:123.45.32.18, port:23456 # tuple version
567 # main version
经过@singledispatch装饰后,在调用connect函数时,最终调用的哪个函数就非常清楚,一目了然了!dispatch的依据,就是函数定义时的第1个参数的type类型。
@singledispatch是从python3.4开始的,因此还有一种写法:
from functools import singledispatch
@singledispatch
def connect(address):
print(address)
@connect.register(str)
def _(addr):
ip, port = addr.split(':')
print(f'IP:{ip}, port:{port}')
@connect.register(tuple)
def _(addr):
ip, port = addr
print(f'IP:{ip}, port:{port}')
connect('123.45.67.18:12345')
connect(('123.45.32.18', 23456))
connect(567)
这是在衍生出来的register装饰器后面带上第1个参数的类型,不过,建议使用第一种typing风格的写法。
Python提供的这种单泛型机制,可以实现在不同类型的第1个参数时,函数参数个数的不同。用这个机制,连参数的默认值都不需要,直接用相同函数名,因第1个参数的类型,而带来的参数个数的不同!
from functools import singledispatch
@singledispatch
def connect(address, info=''):
print(address)
print(info)
@connect.register(str)
def _(addr, a, b, c):
ip, port = addr.split(':')
print(f'IP:{ip}, port:{port}')
print(a,b,c)
@connect.register(tuple)
def _(addr):
ip, port = addr
print(f'IP:{ip}, port:{port}')
connect('123.45.67.18:12345', 1,2,3)
connect(('123.45.32.18', 23456))
connect(567, 'original')
主函数connect可以接收2个参数,当第1个参数为str类型时,要接收4个参数,它们都没有默认值,当第1个参数为tuple类型时,就只有1个参数。Python真的TTMD灵活了......
以上代码运行效果如下:
IP:123.45.67.18, port:12345
1 2 3
IP:123.45.32.18, port:23456
567
original
虽然Python是动态语言,但标准库中提供的@singledispatch还是有价值的,用好了,对代码的可读性和可维护性都有提高!更多关于single dispatch的资料:https://peps.python.org/pep-0443/
我在反复学习Python装饰器和闭包之后,写出了一个模仿singledispatch的onedispatch,供各位同学参考:
from functools import update_wrapper
def onedispatch(func):
func_map = {}
func_map.update({'default': func})
def wrapper(*args):
f = func_map.get(type(args[0]))
if f is None:
f = func_map.get('default')
f(*args)
def reg(argtype):
def deco(func):
func_map.update({type(argtype()): func})
return wrapper
return deco
wrapper.register = reg
return update_wrapper(wrapper, func)
@onedispatch
def test(a,b,c):
""" a self-made single dispatch test """
print('int', a+b+c)
@test.register(str)
def _(a,b,c):
print('str', a+b+c)
@test.register(tuple)
def _(a,b,c):
print('tuple', str(a)+str(b)+str(c))
test(1,2,3)
test('1','2','3')
test((1,2),(3,4),(5,6))
test(5,6,7)
print('--------')
@onedispatch
def m2(a):
""" multiply 2 """
print('int', a*2)
@m2.register(str)
def _(a):
print('str', a*2)
@m2.register(list)
def _(a):
print('list', a*2)
m2(12)
m2('12')
m2([1,2])
m2(99)
运行效果符合预期:
$ python3 deco.py
int 6
str 123
tuple (1, 2)(3, 4)(5, 6)
int 18
--------
int 24
str 1212
list [1, 2, 1, 2]
int 198
心情激动的我,本想继续写一个所谓的fulldispatch装饰器,让python也能像C++那样,按照接口所有参数的类型重载函数。尝试了一下下,又仔细分析了半天,才发现这是办不到的。因为python支持的keyword argument,在传递函数接口的时候,可以是任意顺序,两个类型不同的keyword argument如果按不同的顺序传入接口,接口参数的类型顺序就会不一样。而且,只能用参数类型来判断接口,不能使用参数名称,后者在重载的时候,可以是任意名称(通过function annotations获取函数参数名称和类型)。这样看来,python官方只提供了singledispatch,是有道理的。使用singledispatch,好好设计接口,也基本上能够满足绝大部分的需求了。
本文链接:https://cs.pynote.net/sf/python/202204063/
-- EOF --
-- MORE --