查看: 132|回复: 3

Python functools模块wraps lru_cache partial singledispatch使用详解

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
functools 是 Python 标准库中专注于高阶函数和函数式编程的模块。开发装饰器、缓存、偏函数或类型分派时,活用 functools 能大幅简化代码并提升性能。本文梳理其中最常用的四个工具:wraps、lru_cache、partial 和 singledispatch,并提供综合案例与避坑指南。

1. @functools.wraps — 保留装饰器元信息

编写装饰器时,被装饰函数的 __name__、__doc__、__annotations__ 等信息会被 wrapper 函数覆盖。使用 @wraps 可以将原始函数的这些元属性拷贝回来。
  1. import functools
  2. def log_call(func):
  3.     @functools.wraps(func)
  4.     def wrapper(*args, **kwargs):
  5.         print(f"调用 {func.__name__}")
  6.         return func(*args, **kwargs)
  7.     return wrapper
  8. @log_call
  9. def greet(name: str) -> str:
  10.     """向某人打招呼"""
  11.     return f"你好, {name}"
  12. print(greet.__name__)   # greet(不加 @wraps 会输出 wrapper)
  13. print(greet.__doc__)    # 向某人打招呼
  14. print(greet.__annotations__)  # {'name': <class 'str'>, 'return': <class 'str'>}
复制代码

2. @functools.lru_cache — 记忆化缓存

该装饰器自动缓存函数的计算结果,后续相同参数调用直接返回缓存值。非常适合纯函数、递归或计算密集型场景。
  1. import functools
  2. import time
  3. @functools.lru_cache(maxsize=128)
  4. def fib(n: int) -> int:
  5.     """带缓存的斐波那契"""
  6.     if n < 2:
  7.         return n
  8.     return fib(n - 1) + fib(n - 2)
  9. start = time.time()
  10. print(fib(100))          # 354224848179261915075
  11. print(f"耗时: {time.time() - start:.6f}s")  # 近乎瞬间
  12. 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 固定原函数的某些参数,生成一个新函数。常用于回调、配置重复参数等场景。
  1. import functools
  2. def power(base: float, exp: float) -> float:
  3.     return base ** exp
  4. square = functools.partial(power, exp=2)
  5. cube = functools.partial(power, exp=3)
  6. print(square(5))  # 25
  7. print(cube(5))    # 125
  8. # 排序中固定 key 的索引
  9. data = [(1, "z"), (3, "a"), (2, "m")]
  10. sorted(data, key=functools.partial(lambda k, i: k[i], i=1))
  11. # 结果:[(3, 'a'), (2, 'm'), (1, 'z')]
复制代码

4. @functools.singledispatch — 单分派泛函数

根据第一个参数的类型自动选择对应的函数实现,取代冗长的 if/elif isinstance(...) 链。
  1. import functools
  2. @functools.singledispatch
  3. def serialize(obj):
  4.     raise TypeError(f"不支持的类型: {type(obj)}")
  5. @serialize.register(int)
  6. def _(obj: int) -> str:
  7.     return f"数字: {obj}"
  8. @serialize.register(str)
  9. def _(obj: str) -> str:
  10.     return f"字符串: '{obj}'"
  11. @serialize.register(list)
  12. def _(obj: list) -> str:
  13.     return f"列表: {[serialize(x) for x in obj]}"
  14. @serialize.register(dict)
  15. def _(obj: dict) -> str:
  16.     return f"字典: {{{', '.join(f'{k!r}: {v}' for k, v in obj.items())}}}"
  17. print(serialize(42))     # 数字: 42
  18. print(serialize("hello")) # 字符串: 'hello'
  19. print(serialize([1, "a"])) # 列表: ['数字: 1', "字符串: 'a'"]
复制代码

综合示例:计时 + 缓存装饰器工厂

将 @wraps 与 @lru_cache 组合,同时实现函数元信息保留和结果缓存,并添加耗时打印。
  1. import functools
  2. import time
  3. from typing import Any
  4. def timer_and_cache(func=None, *, maxsize: int = 128):
  5.     """组合计时与缓存的装饰器工厂"""
  6.     def decorator(f):
  7.         @functools.wraps(f)
  8.         @functools.lru_cache(maxsize=maxsize)
  9.         def wrapper(*args, **kwargs):
  10.             start = time.perf_counter()
  11.             result = f(*args, **kwargs)
  12.             elapsed = time.perf_counter() - start
  13.             print(f"{f.__name__} 耗时 {elapsed:.6f}s")
  14.             return result
  15.         wrapper.original = f
  16.         return wrapper
  17.     return decorator(func) if func else decorator
  18. @timer_and_cache
  19. def compute(n: int) -> int:
  20.     time.sleep(0.5)
  21.     return n * n
  22. print(compute(10))  # 首次约 500ms+
  23. print(compute(10))  # 缓存命中,瞬间返回
  24. 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 代码更简洁、更高效,减少重复逻辑并提升运行时性能。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

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 调用会递归处理元素,有没有遇到类型覆盖顺序的问题?比如注册了抽象基类再注册具体类型时需要注意什么?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

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 思路类似,但专门用在类属性上,很适合那些计算成本高且不会变的属性,比如从数据库加载的配置数据,避免每次访问都重新算。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python functools模块wraps lru_cache partial singledispatch使用详解

感谢楼主的详细分享,写得非常清晰!wraps 和 lru_cache 的组合使用示例很实用,解决了装饰器元信息丢失和递归性能瓶颈的常见痛点。partial 和 singledispatch 的举例也通俗易懂,尤其是 singledispatch 替代繁琐的 isinstance 判断,让代码更优雅。平时写类型分派时我更喜欢用 singledispatch,但遇到需要分派子类的情况有时会踩坑,楼主有没有相关的注意事项?另外,Python 3.9 以后用 cache 替代 lru_cache(maxsize=None) 确实更简洁,这点也提醒得很到位。整体内容对新手进阶和老手回顾都很有帮助,收藏了!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-5 17:58 , Processed in 0.032384 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部