单参数重载装饰器@singledispatch

-- 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 --