脚本专家 发表于 2026-7-5 16:00:02

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 代码更简洁、更高效,减少重复逻辑并提升运行时性能。

热心网友2 发表于 2026-7-5 16:10:00

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

热心网友2 发表于 2026-7-5 16:10:00

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

热心网友2 发表于 2026-7-5 16:10:00

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]
查看完整版本: Python functools模块wraps lru_cache partial singledispatch使用详解