Python functools模块wraps lru_cache partial singledispatch使用详解
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=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.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'"]
综合示例:计时 + 缓存装饰器工厂
将 @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 代码更简洁、更高效,减少重复逻辑并提升运行时性能。
Re: Python functools模块wraps lru_cache partial singledispatch使用详解
楼主的总结非常清晰,把 functools 的四个核心工具讲得很透彻。wraps 保留元信息这点确实经常被新手忽略,很多装饰器写出来调试时 __name__ 变 wrapper 容易造成迷惑。lru_cache 的 cache_info 方法也很实用,能直观看到命中率,对优化纯函数很有帮助。 我平时用 partial 比较多,尤其在回调函数里固定部分参数,代码会简洁很多。singledispatch 之前了解不深,楼主给的多类型序列化示例很直观,比写一串 isinstance 好维护。综合示例把 wraps 和 lru_cache 组合起来形成工厂模式,这个思路很适合封装通用工具。 有个小疑问:楼主在使用 singledispatch 注册 list 和 dict 时,list 内部的 serialize 调用会递归处理元素,有没有遇到类型覆盖顺序的问题?比如注册了抽象基类再注册具体类型时需要注意什么?Re: Python functools模块wraps lru_cache partial singledispatch使用详解
楼主的总结非常清晰,把 functools 这四个最实用的工具讲透了。我自己经常在写递归或者重复计算的函数时用 lru_cache,确实能把指数级复杂度降到线性,特别是像斐波那契这种例子,效果立竿见影。wraps 那个细节也很重要,写装饰器如果不加 wraps,调试时看到 __name__ 变成 wrapper 会让人迷惑。 另外想补充一点楼里没展开的:singledispatch 在 Python 3.11 之后支持了 Union 类型和类型变量,如果遇到需要处理 Optional 这类情况,注册时可以直接用 `@serialize.register(int | None)`,比写两个 register 要简洁。还有 partial 配合 map 或者 sorted 时固定参数确实省事,不过要注意如果固定了可变对象(比如列表),会有共享引用的问题,楼主在避坑指南里提了一下参数可哈希性,这个点对新手挺关键。 不知道楼主有没有试过 functools.cached_property?它和 lru_cache 思路类似,但专门用在类属性上,很适合那些计算成本高且不会变的属性,比如从数据库加载的配置数据,避免每次访问都重新算。Re: Python functools模块wraps lru_cache partial singledispatch使用详解
感谢楼主的详细分享,写得非常清晰!wraps 和 lru_cache 的组合使用示例很实用,解决了装饰器元信息丢失和递归性能瓶颈的常见痛点。partial 和 singledispatch 的举例也通俗易懂,尤其是 singledispatch 替代繁琐的 isinstance 判断,让代码更优雅。平时写类型分派时我更喜欢用 singledispatch,但遇到需要分派子类的情况有时会踩坑,楼主有没有相关的注意事项?另外,Python 3.9 以后用 cache 替代 lru_cache(maxsize=None) 确实更简洁,这点也提醒得很到位。整体内容对新手进阶和老手回顾都很有帮助,收藏了!
页:
[1]