Python 中 singledispatch 单分派泛函数

FreeGuideOnline 最新 2026-07-08

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 装饰器为 intliststr 注册了不同的实现。注意注册方式有两种:使用类型注解(例如 obj: int)或直接向 register 传入类型(例如 register(str))。函数名通常用下划线 _,因为我们只用它的类型提示和注册功能,并不关心名字。

工作原理

singledispatch 的调度基于第一个参数的实际类型进行查找。它会维护一个从类型到具体实现的注册表。当调用时,分派算法会按照以下顺序查找匹配:

  1. 检查传入对象的准确类型是否注册。
  2. 若没有,检查该对象的类的所有父类(按照方法解析顺序 MRO),直到找到注册的父类。
  3. 如果还没找到,调用之前通过 @singledispatch 装饰的默认函数

这意味着你可以利用继承体系来减少重复注册。例如,如果你为 numbers.Number 注册了一个实现,那么所有数字子类(intfloatcomplex)在没有更具体注册的情况下都会命中这个实现。

实战案例:一个简易的值格式化器

假设我们在开发一个调试工具,需要把不同类型的值转换成友好的字符串表示。

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