Python 开发技巧:functools 模块深入

知识点简介

functools 是 Python 标准库中专门为高阶函数服务的模块,内置了一系列用于函数式编程和装饰器开发的实用工具。掌握 functools 能让你写出更简洁、更 Pythonic、性能更好的代码。本文覆盖最常用的 4 个工具:@wraps@lru_cachepartial@singledispatch

核心工具详解

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

写装饰器时,被装饰的函数会丢失原名、文档字符串、注解等信息。@wraps 负责把这些元信息拷贝回来。

python 复制代码
import functools

def log_call(func):
    @functools.wraps(func)  # 保留 func 的 __name__, __doc__ 等
    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 --- 记忆化缓存

自动缓存函数的返回值,相同参数再次调用直接返回缓存结果。适合纯函数和无副作用的递归或计算密集型操作。

python 复制代码
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)

注意@lru_cache 的参数必须是可哈希 的。如果参数是 list、dict 等可变类型,需要先转成 tuple 或使用 @cache(Python 3.9+)。

3. functools.partial --- 偏函数冻结

固定一个函数的某些参数,生成一个新函数。适用于回调、配置化场景。

python 复制代码
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(...) 的链式判断。

python 复制代码
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'"]

综合使用示例

python 复制代码
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, ...)

注意事项 / 避坑指南

  1. @lru_cache 不要装饰有副作用的方法:如果函数依赖外部状态(文件、数据库、全局变量),缓存会导致返回过期结果。

  2. @wraps 不是万能的 :它仅拷贝 __module____name____qualname____annotations____doc____dict__。签名信息(inspect.signature)不会自动恢复,如需保留完整签名配合 @decorator 库或 inspect 手动处理。

  3. partial 对象不是普通函数functools.partial 返回的对象没有 __name__ 属性(除非手动设置),也不支持注解。检查类型时用 callable() 而非 isfunction()

  4. singledispatch 只对第一个参数生效 :如果需要根据多个参数分派,考虑 functools.singledispatchmethod(Python 3.8+)或使用类型模式匹配(Python 3.10+)。

  5. 缓存泄漏内存@lru_cache(maxsize=None) 会无限增长,慎用。对于预期大量不同参数调用的场景,设置合理的 maxsize 或考虑 @cache + 手动清除策略。

相关推荐
Csvn1 小时前
Python 开发技巧 · Python 上下文管理器 —— 从 with 到 contextlib 实战
后端
行者全栈架构师1 小时前
PolarDB + Spring Boot 实战:从自建MySQL到云原生数据库的零停机迁移
java·后端·架构
Gopher_HBo1 小时前
moby-容器对象与状态学习
后端
xiaoshuai10241 小时前
Controller 直连了数据库、模块缠成死结:用 ArchUnit 把架构钉死
后端
陈随易13 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·后端·程序员
IT_陈寒15 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰16 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
用户83562907805117 小时前
Python 实现 PDF 文件加密与解密方法
后端·python