Python singledispatch 单分派泛型函数详解与多分派实现
本文深入解析 Python 标准库 functools 模块中的 singledispatch 装饰器,演示如何基于第一个参数类型实现单分派泛型函数。通过 register 方法注册不同类型处理逻辑,并探讨利用 isinstance 手动实现多分派泛型函数的技巧,同时澄清 Python 类型提示与真正泛型的区别。
@functools.singledispatch
该装饰器用于将普通函数转换为支持单一分派的泛型函数。
通过 @singledispatch 装饰一个函数,即可定义一个泛型函数。需要注意的是,泛型分派的依据是第一个参数的类型。
from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
if verbose:
print("Let me just say,", end=" ")
print(arg)利用泛型函数的 register() 属性可以注册新的实现以覆盖默认行为。register() 本身也是一个装饰器,对于带有类型注释的函数,它会自动匹配第一个参数类型为指定类型的重载函数。
@fun.register
def _(arg: int, verbose=False):
if verbose:
print("Strength in numbers, eh?", end=" ")
print(arg)
@fun.register
def _(arg: list, verbose=False):
if verbose:
print("Enumerate this:")
for i, elem in enumerate(arg):
print(i, elem)调用泛型函数时,系统会根据第一个参数的实际类型,分派并执行对应的具体实现函数。
当第一个参数为 int 类型时的执行结果:
>>> fun(42, verbose=True) Strength in numbers, eh? 42
当第一个参数为 list 类型时的执行结果:
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True) Enumerate this: 0 spam 1 spam 2 eggs 3 spam
若通过 register() 装饰的函数未包含类型注释,则可以在 register() 调用时显式指定所需的类型参数。
@fun.register(complex)
def _(arg, verbose=False):
if verbose:
print("Better than complicated.", end=" ")
print(arg.real, arg.imag)>>>fun(6+5j, verbose=True) Better than complicated. 6.0 5.0
为了注册已存在的函数或匿名函数,register() 还可以作为普通函数调用使用。
def nothing(arg, verbose=False):
print("Nothing.")
fun.register(type(None), nothing)
fun.register(int, lambda x, y, verbose=False: x+y) # 本人添加的,官网没有这个例子注:经测试,若存在多个可分派的实现,泛型函数将选择定义顺序中最后注册的那个函数进行分派。
>>> fun(None) Nothing. >>>fun(1,2) 3
该 register() 属性返回一个未被装饰的原始函数,此函数用于激活装饰器的堆栈空间,并为其创建独立的测试运行单元。
>>>import decimal
>>> @fun.register(float)
... @fun.register(decimal.Decimal)
... def fun_num(arg, verbose=False):
... if verbose:
... print("Half of your number:", end=" ")
... print(arg / 2)
...
>>> fun_num is fun
False如果针对特定具体类型未注册对应的实现函数,泛型函数将回溯查找更通用的实现。由于使用 @singledispatch 装饰的原始函数默认注册了 object 类型,因此在找不到更匹配的实现时,将回退调用原始函数。
注:此示例由作者提供。
>>>fun(bool,verbose=True) Let me just say, <class 'bool'>
可以使用只读属性 registry 来查看当前已注册的所有类型及其对应的函数实现。
>>> fun.registry.keys()
dict_keys([<class 'object'>, <class 'decimal.Decimal'>, <class 'float'>, <class 'int'>, <class 'list'>, <class 'complex'>, <class 'NoneType'>])
>>> fun.registry
{<class 'object'>: <function fun at 0x00000225F21AC268>, <class 'decimal.Decimal'>: <function fun_num at 0x00000225F2517378>, <class 'float'>: <function fun_num at 0x00000225F2517378>, <class 'int'>: <function <lambda> at 0x00000225F2596488>, <class 'list'>: <function _ at 0x00000225F25172F0>, <class 'complex'>: <function _ at 0x00000225F2596400>, <class 'NoneType'>: <function nothing at 0x00000225F258E400>}
>>> fun.registry[float]
<function fun_num at 0x00000225F2517378>
>>>fun.registry[object]
<function fun at 0x00000225F21AC268>官方文档链接:https://docs.python.org/3/library/functools.html?highlight=functools wraps#functools.update_wrapper
singledispatch 实现单分派泛型函数及多分派泛型函数
本次的主题是探索相对少见的泛型应用!
提到泛型,Java 开发者可能非常熟悉,其本质是参数化类型,即操作的数据类型被指定为一个参数。但对于 Python 开发者而言,这部分知识或使用经验可能较为陌生。
那么,在 Python 中如何实现泛型呢?
确实有一个库可以实现这一功能。
我们首先导入包含 singledispatch 的库:
from functools import singledispatch
该库仅支持针对函数的第一个参数进行泛型分派。


首先定义一个主函数并使用 singledispatch 进行修饰,作为基础实现。随后定义一些“子函数”,使用 @主函数名.register 作为修饰器,并传入一个参数作为类型判断依据(该参数即为子函数第一个参数的类型,也是主函数第一个参数的类型)。注意:这里的子函数通常无需命名,因为它们在泛型函数内部使用,且参数结构与主函数保持一致。
然而,这种单分派的局限性较大,实际应用价值有限,我们需要进一步推广到多分派泛型函数。
多分派泛型函数的实现方法(由于 Python 原生仅支持对第一个参数进行泛型判断,因此需要添加额外代码来实现多分派)。


我们在单分派的基础上,结合 isinstance 进行类型判断,以确保其他参数的类型一致性。
上述多分派泛型函数也可以采用以下写法:

每个子函数使用了两个修饰器,但这两个修饰器均针对第一个参数生效。
你以为这就结束了吗?
Python 3.5 引入了新特性——在参数后添加冒号指定类型,并在函数定义后使用 -> 指定返回值类型。
冒号表示该参数预期的类型,箭头表示函数的返回类型。

这虽然指定了参数类型,但即使传入的类型与冒号后指定的类型不一致,也不会报错(除非存在语法错误),因此这并不等同于真正的泛型。
但它与泛型存在关联,这涉及 register 修饰器的第二种用法。


省略 register 的参数,转而使用冒号符号进行类型指定。
内容完结。
以上为个人经验总结,希望能为大家提供参考,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









