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时,11.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 依然是旧值

对策

  1. 明确该属性是否真的适合缓存。
  2. 如果必须缓存,在修改数据时手动删除缓存: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 开发中:

  1. 性能:善用@lru_cache@cached_property
  2. 架构:用singledispatch解耦业务逻辑。
  3. 规范:永远使用@wraps保护你的装饰器。