functools 是 Python 标准库中专注于高阶函数和函数式编程的模块。开发装饰器、缓存、偏函数或类型分派时,活用 functools 能大幅简化代码并提升性能。本文梳理其中最常用的四个工具:wraps、lru_cache、partial 和 singledispatch,并提供综合案例与避坑指南。
1. @functools.wraps — 保留装饰器元信息
编写装饰器时,被装饰函数的 __name__、__doc__、__annotations__ 等信息会被 wrapper 函数覆盖。使用 @wraps 可以将原始函数的这些元属性拷贝回来。
- import functools
- def log_call(func):
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- print(f"调用 {func.__name__}")
- return func(*args, **kwargs)
- return wrapper
- @log_call
- def greet(name: str) -> str:
- """向某人打招呼"""
- return f"你好, {name}"
- print(greet.__name__) # greet(不加 @wraps 会输出 wrapper)
- print(greet.__doc__) # 向某人打招呼
- print(greet.__annotations__) # {'name': <class 'str'>, 'return': <class 'str'>}
复制代码
2. @functools.lru_cache — 记忆化缓存
该装饰器自动缓存函数的计算结果,后续相同参数调用直接返回缓存值。非常适合纯函数、递归或计算密集型场景。
- import functools
- import time
- @functools.lru_cache(maxsize=128)
- def fib(n: int) -> int:
- """带缓存的斐波那契"""
- if n < 2:
- return n
- return fib(n - 1) + fib(n - 2)
- start = time.time()
- print(fib(100)) # 354224848179261915075
- print(f"耗时: {time.time() - start:.6f}s") # 近乎瞬间
- print(fib.cache_info()) # CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
复制代码
⚠️ 注意:参数必须是可哈希类型。若参数含 list/dict,先转为 tuple。Python 3.9+ 可用 @functools.cache 作为无大小限制的简写。
3. functools.partial — 偏函数冻结
partial 固定原函数的某些参数,生成一个新函数。常用于回调、配置重复参数等场景。
- import functools
- def power(base: float, exp: float) -> float:
- return base ** exp
- square = functools.partial(power, exp=2)
- cube = functools.partial(power, exp=3)
- print(square(5)) # 25
- print(cube(5)) # 125
- # 排序中固定 key 的索引
- data = [(1, "z"), (3, "a"), (2, "m")]
- sorted(data, key=functools.partial(lambda k, i: k[i], i=1))
- # 结果:[(3, 'a'), (2, 'm'), (1, 'z')]
复制代码
4. @functools.singledispatch — 单分派泛函数
根据第一个参数的类型自动选择对应的函数实现,取代冗长的 if/elif isinstance(...) 链。
- import functools
- @functools.singledispatch
- def serialize(obj):
- raise TypeError(f"不支持的类型: {type(obj)}")
- @serialize.register(int)
- def _(obj: int) -> str:
- return f"数字: {obj}"
- @serialize.register(str)
- def _(obj: str) -> str:
- return f"字符串: '{obj}'"
- @serialize.register(list)
- def _(obj: list) -> str:
- return f"列表: {[serialize(x) for x in obj]}"
- @serialize.register(dict)
- def _(obj: dict) -> str:
- return f"字典: {{{', '.join(f'{k!r}: {v}' for k, v in obj.items())}}}"
- print(serialize(42)) # 数字: 42
- print(serialize("hello")) # 字符串: 'hello'
- print(serialize([1, "a"])) # 列表: ['数字: 1', "字符串: 'a'"]
复制代码
综合示例:计时 + 缓存装饰器工厂
将 @wraps 与 @lru_cache 组合,同时实现函数元信息保留和结果缓存,并添加耗时打印。
- import functools
- import time
- from typing import Any
- def timer_and_cache(func=None, *, maxsize: int = 128):
- """组合计时与缓存的装饰器工厂"""
- def decorator(f):
- @functools.wraps(f)
- @functools.lru_cache(maxsize=maxsize)
- def wrapper(*args, **kwargs):
- start = time.perf_counter()
- result = f(*args, **kwargs)
- elapsed = time.perf_counter() - start
- print(f"{f.__name__} 耗时 {elapsed:.6f}s")
- return result
- wrapper.original = f
- return wrapper
- return decorator(func) if func else decorator
- @timer_and_cache
- def compute(n: int) -> int:
- time.sleep(0.5)
- return n * n
- print(compute(10)) # 首次约 500ms+
- print(compute(10)) # 缓存命中,瞬间返回
- print(compute.cache_info()) # CacheInfo(hits=1, misses=1, ...)
复制代码
避坑指南
- @lru_cache 不要装饰有副作用的函数(如读写文件、修改全局变量),否则缓存会返回过期数据。
- @wraps 仅恢复 __module__、__name__、__qualname__、__annotations__、__doc__ 和 __dict__,不会恢复函数签名(inspect.signature)。如需完整签名,可配合 decorator 库或手动处理。
- functools.partial 返回的对象没有 __name__ 属性(除非手动设置),也不支持注解。判断是否可调用应用 callable() 而非 isfunction()。
- singledispatch 只对第一个参数进行类型分派。若需多参数分派,Python 3.8+ 可用 singledispatchmethod,3.10+ 可用 match/case 模式匹配。
- 缓存可能导致内存泄漏:@lru_cache(maxsize=None) 会无限增长。对于参数变化频繁的场景,设置合理 maxsize 或定期清除缓存。
掌握以上四个 functools 工具,能让你的 Python 代码更简洁、更高效,减少重复逻辑并提升运行时性能。 |