Python 中 singledispatch 单分派泛函数
python from functools import singledispatch
### 第一个泛函数
让我们从一个简单的例子开始:创建一个 `describe` 函数,对于不同的类型会返回不同的描述字符串。
```python
from functools import singledispatch
@singledispatch
def describe(obj):
"""默认实现,用于没有注册的类型"""
return f"未知类型: {type(obj).__name__}"
@describe.register
def _(obj: int):
return f"整数: {obj}"
@describe.register
def _(obj: list):
return f"列表,长度: {len(obj)}"
@describe.register(str)
def _(obj):
return f"字符串,内容: '{obj}'"
测试一下:
print(describe(42)) # 整数: 42
print(describe([1, 2, 3])) # 列表,长度: 3
print(describe("hello")) # 字符串,内容: 'hello'
print(describe(3.14)) # 未知类型: float
这里 @singledispatch 装饰了 describe 函数作为泛函数的默认实现。然后我们使用 @describe.register 装饰器为 int、list、str 注册了不同的实现。注意注册方式有两种:使用类型注解(例如 obj: int)或直接向 register 传入类型(例如 register(str))。函数名通常用下划线 _,因为我们只用它的类型提示和注册功能,并不关心名字。
工作原理
singledispatch 的调度基于第一个参数的实际类型进行查找。它会维护一个从类型到具体实现的注册表。当调用时,分派算法会按照以下顺序查找匹配:
- 检查传入对象的准确类型是否注册。
- 若没有,检查该对象的类的所有父类(按照方法解析顺序 MRO),直到找到注册的父类。
- 如果还没找到,调用之前通过
@singledispatch装饰的默认函数。
这意味着你可以利用继承体系来减少重复注册。例如,如果你为 numbers.Number 注册了一个实现,那么所有数字子类(int、float、complex)在没有更具体注册的情况下都会命中这个实现。
实战案例:一个简易的值格式化器
假设我们在开发一个调试工具,需要把不同类型的值转换成友好的字符串表示。
from functools import singledispatch
from datetime import datetime, date
from collections.abc import Mapping, Iterable
@singledispatch
def format_value(value):
return repr(value)
@format_value.register
def _(value: datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
@format_value.register
def _(value: date):
return value.strftime("%Y-%m-%d")
@format_value.register(Mapping) # 字典或类似映射
def _(value):
items = [f"{k}: {format_value(v)}" for k, v in value.items()]
return "{" + ", ".join(items) + "}"
@format_value.register(Iterable) # 列表、元组等,但不包含字符串和映射
def _(value):
items = [format_value(item) for item in value]
return "[" + ", ".join(items) + "]"
# 对字符串进行特殊处理,避免被Iterable误伤
@format_value.register(str)
def _(value):
return f'"{value}"'
# 使用
print(format_value(42))
print(format_value(datetime.now()))
print(format_value({"name": "Alice", "age": 30}))
print(format_value([1, 2, 3, {"key": "value"}]))
输出可能为:
42
2025-03-28 14:22:10
{name: "Alice", age: 30}
[1, 2, 3, {key: "value"}]
注意点:
str也属于Iterable,但因为我们在注册时把str放在更具体的注册中,并且singledispatch会优先匹配准确类型,所以字符串不会被Iterable的处理分支捕获。- 为了避免此类冲突,通常应该先注册更具体的类型,再注册更宽泛的抽象基类。
注册已存在的函数
除了装饰器,你还可以使用 register() 方法将任何已有函数注册为某种类型的实现。
from functools import singledispatch
@singledispatch
def process(data):
print("默认处理")
def process_int(data: int):
print(f"处理整数: {data}")
def process_str(data: str):
print(f"处理字符串: {data}")
# 可以像这样注册
process.register(int, process_int)
process.register(str, process_str)
# 或者使用 register 返回的装饰器
process.register(float)(lambda f: print("浮点数!"))
这在你重构代码、或者需要从外部模块注册时非常有用。
类型注解与抽象基类
Python 的 singledispatch 支持基于类型注解的分派,但不要被误导:分派仍然只看第一个参数的类型,而不检查注解中的其他内容(如 -> bool)。所以即使你写了 def _(a: int) -> bool:,它仍然只根据 int 进行分派。
抽象基类(ABC)的运用可以极大提高泛函数的灵活性。例如为 numbers.Number 注册一次,覆盖所有数字。
from numbers import Number
from functools import singledispatch
@singledispatch
def double(x):
return x + x # 可能适用于字符串、列表等
@double.register(Number)
def _(x):
return 2 * x
print(double(5)) # 10
print(double(3.14)) # 6.28
print(double("Hi")) # "HiHi"
获取已注册的实现
你可以通过 dispatch() 方法查看注册表,甚至通过类型获取对应的实现函数。
print(describe.dispatch(int)) # <function _ at 0x...>
print(describe.dispatch(float)) # <function describe at 0x...> (因为未注册float,返回默认函数)
# 查看注册表
print(describe.registry)
# dict_keys([object, int, list, str])
默认实现总是注册在 object 键下面,所以 registry 中会看到 object。
针对多个类型的“栈式”注册
在 Python 3.11+ 中,singledispatch 支持使用多个装饰器为不同类型注册同一个实现(即一个函数可以被多个类型共用)。你可以通过多次调用 register 来实现:
@format_value.register(int)
@format_value.register(float)
@format_value.register(complex)
def _(value):
return format(value, ".2f")
print(format_value(3.14159)) # 3.14
print(format_value(42)) # 42.00
如果你的 Python 版本低于 3.11,则需要将同一个函数分别用 .register() 注册,或使用抽象基类 Number 来一次覆盖。
高级技巧:支持方法内部的单分派
singledispatch 主要设计用于函数,但在类方法中也可以使用。注意必须使用 @singledispatchmethod(从 Python 3.8 开始)。
from functools import singledispatchmethod
class Handler:
@singledispatchmethod
def handle(self, arg):
raise NotImplementedError("不支持的类型")
@handle.register
def _(self, arg: int):
print(f"处理整数: {arg}")
@handle.register
def _(self, arg: str):
print(f"处理字符串: {arg}")
h = Handler()
h.handle(10) # 处理整数: 10
h.handle("ok") # 处理字符串: ok