functools 包详解
定位:
functools是 Python 函数式编程与装饰器体系的基石。它解决了“如何安全、高效地操作函数”这一核心问题。核心价值:减少重复代码、提升运行效率、规范装饰器写法。
第一部分:核心 API 全景图谱
| 类别 | 工具 | 一句话解释 | 使用频率 |
|---|---|---|---|
| 缓存 | @lru_cache | 记住函数结果,避免重复计算 | ⭐⭐⭐⭐⭐ |
@cached_property | 把昂贵的计算变成“只读属性” | ⭐⭐⭐⭐ | |
| 变换 | partial | 冻结参数,制造新函数 | ⭐⭐⭐⭐ |
partialmethod | 类中的partial | ⭐⭐⭐ | |
| 装饰器 | @wraps | 保护原函数的“身份证” | ⭐⭐⭐⭐⭐ |
update_wrapper | @wraps的底层实现 | ⭐⭐ | |
| 分发 | singledispatch | 根据类型重载函数 | ⭐⭐⭐ |
singledispatchmethod | 类方法中的类型重载 | ⭐⭐⭐ | |
| 辅助 | reduce | 累积计算(左折叠) | ⭐⭐ |
cmp_to_key | 兼容旧式比较函数 | ⭐⭐ | |
@total_ordering | 简化类的比较运算符 | ⭐⭐ |
第二部分:核心工具深度解析
1. 缓存机制(性能利器)
@lru_cache(Least Recently Used)
原理:哈希表存储结果,链表维护访问顺序。当缓存满时,淘汰最久未使用的项。
fromfunctoolsimportlru_cache@lru_cache(maxsize=128,typed=False)deffibonacci(n):ifn<2:returnnreturnfibonacci(n-1)+fibonacci(n-2)# 缓存管理fibonacci.cache_clear()# 清空info=fibonacci.cache_info()# 命中率监控maxsize: 缓存数量。None为无限(慎用,内存泄漏风险)。typed:True时,1和1.0视为不同参数。- 前提:参数必须可哈希(Hashable)。
@cached_property
原理:首次访问时计算,结果存入实例字典;后续访问直接返回实例字典中的值。
fromfunctoolsimportcached_propertyclassDataPipeline:def__init__(self,raw_data):self.raw_data=raw_data@cached_propertydefcleaned_data(self):print("Expensive cleaning...")return[x.strip()forxinself.raw_data]obj=DataPipeline([" a "," b "])obj.cleaned_data# 计算一次obj.cleaned_data# 直接取值- 区别:不同于
@property(每次访问都计算),也不同于手动_cache = None(代码臃肿)。
2. 函数变换(逻辑复用)
partial(偏函数)
原理:冻结函数的部分位置参数或关键字参数,返回一个新的可调用对象。
fromfunctoolsimportpartialdefsend_request(method,url,headers=None,timeout=10):...# 创建一个新的“专用”函数post_json=partial(send_request,"POST",headers={"Content-Type":"application/json"})# 调用时只需传入剩余参数post_json("/api/users",timeout=30)- 优势:比
lambda可读性更强,且保留了函数的__name__属性。
partialmethod
专门用于类的方法定义,避免子类重写时的样板代码。
fromfunctoolsimportpartialmethodclassConnection:defconnect(self,proto,host,port):...http=partialmethod(connect,"http",port=80)https=partialmethod(connect,"https",port=443)3. 装饰器安全(@wraps)
痛点:装饰器会覆盖原函数的元信息(__name__,__doc__),导致日志、调试和反射出错。
fromfunctoolsimportwrapsdefmy_decorator(func):@wraps(func)# 关键!defwrapper(*args,**kwargs):"""Wrapper doc"""returnfunc(*args,**kwargs)returnwrapper@my_decoratordefimportant_func():"""Important doc"""passprint(important_func.__name__)# important_func (而不是 wrapper)print(important_func.__doc__)# Important doc黄金法则:写装饰器,必加@wraps。
4. 函数分发(替代 If/Elif)
singledispatch
原理:根据第一个参数的类型,分发到不同的实现函数。
fromfunctoolsimportsingledispatch@singledispatchdefserialize(obj):raiseTypeError(f"Unknown type:{type(obj)}")@serialize.registerdef_(obj:dict):returnf"Dict:{obj}"@serialize.registerdef_(obj:list):returnf"List:{len(obj)}items"- 好处:符合开闭原则。新增类型支持时,无需修改原主函数。
singledispatchmethod
用于类中的方法分发,常见于 Form 验证或 API 序列化。
第三部分:Web 开发实战(FastAPI & Django)
1. FastAPI 场景
场景 A:全局配置(官方推荐)
痛点:每次请求都读取磁盘或环境变量非常低效。
# config.pyfromfunctoolsimportlru_cache@lru_cache(maxsize=1)defget_settings():# 假设这是一个昂贵的 I/O 操作return{"db_url":"sqlite:///test.db"}# main.py@app.get("/")defread_root(settings=Depends(get_settings)):returnsettings场景 B:依赖注入参数化
痛点:同一个认证逻辑,Admin 和普通用户需要的权限不同。
fromfastapiimportDependsfromfunctoolsimportpartialdefget_user(token:str,role:str):...get_admin=partial(get_user,role="admin")get_user_dep=partial(get_user,role="user")@app.get("/admin",dependencies=[Depends(get_admin)])defadmin_dashboard():...场景 C:响应序列化
痛点:避免 View 层充斥大量的isinstance判断。
# 使用 singledispatch 统一处理不同类型的返回值@singledispatchdefto_json_response(data):returnJSONResponse(content=str(data))@to_json_response.register(User)def_(user:User):returnJSONResponse(content={"id":user.id,"name":user.name})2. Django 场景
场景 A:模板 Tags(防 N+1)
痛点:模板循环中重复查询数据库。
# templatetags/my_tags.pyfromdjangoimporttemplatefromfunctoolsimportlru_cache register=template.Library()@register.simple_tag@lru_cache(maxsize=256)defget_category_name(category_id):# 这个查询在单次渲染中被缓存returnCategory.objects.get(id=category_id).name场景 B:Signals 参数固化
痛点:Django Signal 只能接收(sender, instance, **kwargs),无法直接传参。
fromfunctoolsimportpartialfromdjango.db.models.signalsimportpost_savedefnotify_channel(instance,channel):send_notification(instance,channel)# 固化参数notify_email=partial(notify_channel,channel='email')notify_sms=partial(notify_channel,channel='sms')post_save.connect(notify_email,sender=Order)场景 C:Middleware 装饰器
痛点:自定义中间件导致 View 元数据丢失。
defrole_required(view_func):@wraps(view_func)# 必须,否则 DRF/Admin 报错def_wrapped(request,*args,**kwargs):ifnotrequest.user.is_staff:returnHttpResponseForbidden()returnview_func(request,*args,**kwargs)return_wrapped第四部分:避坑指南(非常重要)
❌ 坑 1:lru_cache缓存了“请求级”数据
现象:用户 A 看到用户 B 的数据。
原因:Web 服务器是常驻进程,lru_cache默认是全局的。
对策:只缓存全局不变数据(如配置、常量、静态映射)。用户相关数据使用cached_property或 Session。
❌ 坑 2:cached_property遇到可变对象
现象:对象内容变了,但缓存值没变。
代码:
classCart:@cached_propertydeftotal(self):returnsum(item.priceforiteminself.items)defadd_item(self,item):self.items.append(item)# total 依然是旧值对策:
- 明确该属性是否真的适合缓存。
- 如果必须缓存,在修改数据时手动删除缓存:
del self.total。
❌ 坑 3:装饰器顺序错误
规则:越靠近函数定义的装饰器,越先执行(包装)。
正确顺序:
# 逻辑上是:用 lru_cache 缓存 被 wraps 保护的 wrapper 的结果@lru_cache@wraps(func)deffunc():...错误顺序:
@wraps(func)@lru_cache# 这里 wraps 缓存了一个可能被包装过的函数,逻辑混乱deffunc():...第五部分:决策速查表
| 需求 | 首选工具 | 理由 |
|---|---|---|
| 递归、DB 查询、API 调用 | @lru_cache | 空间换时间,指数级提速 |
| 昂贵计算的属性 | @cached_property | 延迟加载,请求内复用 |
| 依赖注入/回调函数参数固定 | partial | 代码整洁,意图明确 |
| 写装饰器 | @wraps | 防止元数据丢失(必选) |
替代if/elif类型判断 | singledispatch | 符合开闭原则,易维护 |
类中实现比较大小 (< > ==) | @total_ordering | 少写 4 个魔法方法 |
总结
functools不仅仅是一个工具箱,它代表了一种声明式、组合式的编程思维。在 Web 开发中:
- 性能:善用
@lru_cache和@cached_property。 - 架构:用
singledispatch解耦业务逻辑。 - 规范:永远使用
@wraps保护你的装饰器。
