functools 提供操作可调用对象的高阶工具:预先绑定参数、保留包装器元数据、折叠输入,以及把比较协议接入现有 API。
当已有函数几乎符合目标接口,只差一部分参数、元数据或协议适配时使用它;缓存与单分派分别由专门主题讲解。
先写清最终调用签名与空输入、相等性、覆盖规则,再选择 partial()、wraps()、reduce() 或排序适配器。
是什么,为什么存在
functools 是 Python 标准库中面向高阶函数( higher-order function )和可调用对象的模块。高阶函数接收函数、返回函数,或者两者兼有;模块中的工具也适用于实现了调用协议的其他对象。
这组工具解决的是接口之间的小型结构差异。某个回调槽只接受一个实参,而现有函数还需要类别;装饰器创建了新函数,但检查工具仍应看到原接口;旧代码提供二元比较函数,而 sorted() 需要键函数。functools 把这些适配规则做成可组合对象,不要求复制原函数逻辑。
本文集中讲五组彼此相关的适配器:partial() 与 partialmethod() 绑定实参,wraps() 与 update_wrapper() 维护包装器元数据,reduce() 累积输入,cmp_to_key() 转换比较协议,total_ordering 补齐富比较方法。cache、lru_cache 与 cached_property 属于缓存设计,singledispatch 属于运行时分派;它们分别放在相关主题中,避免在模块概览里重复一套不完整规则。
这些工具不会替你定义业务契约。partial() 不会验证被绑定的值,wraps() 不会证明包装器转发正确,reduce() 不会替空输入选择单位元,cmp_to_key() 也不会修复不一致的比较函数。先确定目标接口,再决定适配器,通常比从工具名称反推用途更可靠。
工具地图
| 工具 | 产生或修改的对象 | 关键契约 |
|---|---|---|
partial() | 带预填实参的新可调用对象 | 调用时的关键字可覆盖预填关键字 |
partialmethod() | 类属性上的描述符 | 实例绑定必须先正确插入 self |
wraps() | 带原函数元数据的包装函数 | 元数据透明不等于行为透明 |
reduce() | 一个最终累积值 | 空输入是否有效由 initial 决定 |
cmp_to_key() | 包装比较逻辑的键对象 | 比较函数必须返回负数、零或正数 |
total_ordering | 补齐比较方法的类 | 相等与排序必须描述同一关系 |
工作原理
partial(func, *args, **keywords) 保存目标可调用对象与一组预填实参。调用所得对象时,新的位置实参接在已保存的位置实参之后;新的关键字与已保存关键字合并,且调用时的同名关键字优先。这个过程叫偏应用( partial application ),它配置调用,不会提前执行 func。
Python 3.14 新增的 functools.Placeholder 可以在已保存的位置实参中预留任意位置。真正调用时,位置实参先从左到右填满所有占位符,其余位置实参再追加到末尾。占位符不能作为关键字值,而且每个占位符都必须被填充。
partialmethod() 解决相同配置出现在类定义中的情况。它是描述符而不是普通可调用对象;访问实例属性时,它先让底层函数完成方法绑定,再应用预填实参。若把普通 partial() 直接放进类体,self 的位置很容易与预填参数发生冲突。
update_wrapper(wrapper, wrapped) 把一组选定属性从被包装对象复制到包装对象,并更新另一组选定属性。默认会处理名称、限定名、文档、注解、类型形参和包装器的 __dict__,还会设置 __wrapped__。wraps(wrapped) 是适合写在包装函数上的便捷装饰器,本质上为 update_wrapper() 预填了参数。
reduce(function, iterable, initial) 从左到右调用二元函数。每一步都把上一次的累积值作为左实参,把下一个输入元素作为右实参;若提供 initial,它位于整条归约链最前面,也是空输入的结果。Python 3.14 允许把 initial 写成关键字,但本文的可运行示例用位置形式,以便在本地 Python 3.12 上核对输出。
排序工具处理两类不同接口。键函数每次接收一个元素并返回可比较的投影; 比较函数(comparator) 每次接收两个元素,并以负数、零、正数表示小于、等于、大于。cmp_to_key() 把后一种接口包装成前一种 API 可以接受的对象,主要用于迁移或接入已有比较协议。
参数适配的流程可以画成一条合并管道:
这张图只描述调用绑定。它没有缓存结果、复制可变对象或锁定预填关键字;这些行为都需要调用方另行实现和测试。
示例
四个示例依次展示函数配置、方法配置、元数据维护,以及归约与比较协议适配。每段输出都来自本地 python3 实际执行。
配置一个事件格式化函数
format_event() 保持通用,invoice_event 则符合只需订单号的调用点。预填关键字 compact=False 仍可在真正调用时覆盖。
from functools import partial
def format_event(category, event_id, *, compact=False):
separator = ":" if compact else " / "
return f"{category}{separator}{event_id:04d}"
invoice_event = partial(format_event, "invoice", compact=False)
print(invoice_event(7))
print(invoice_event(7, compact=True))
print(invoice_event.func.__name__)
print(invoice_event.args)
print(invoice_event.keywords)invoice / 0007
invoice:0007
format_event
('invoice',)
{'compact': False}func、args 与 keywords 让检查工具和调试代码看到偏函数保存的配置。业务代码仍应通过公开调用接口使用它,不要依赖修改这些诊断属性来动态重配行为。
调用方覆盖 compact 是明确的标准行为。如果该值必须固定,偏函数不是权限边界;应在不暴露该关键字的具名包装函数中强制规则,并为错误调用编写测试。
在类定义中配置方法
partialmethod() 让两个类属性复用同一个状态转换实现。底层函数仍按普通实例方法接收 self,预填的状态位于它之后。
from functools import partialmethod
class Ticket:
def __init__(self, ticket_id):
self.ticket_id = ticket_id
self.state = "open"
def transition(self, new_state, *, audit=True):
self.state = new_state
return f"{self.ticket_id}: {new_state}, audit={audit}"
resolve = partialmethod(transition, "resolved")
reopen = partialmethod(transition, "open")
ticket = Ticket(42)
print(ticket.resolve())
print(ticket.reopen(audit=False))
print(ticket.state)42: resolved, audit=True
42: open, audit=False
openresolve 与 reopen 仍表现为绑定方法。调用时可以覆盖预填关键字,但这里预填的是位置参数 new_state,额外再传一个位置状态会因为实参数量冲突而失败。
当每个便捷方法还要执行不同校验、授权或副作用时,直接写具名方法通常更清楚。partialmethod() 适合差异确实只有参数配置的情况,不适合隐藏逐渐分叉的业务流程。
保留装饰器的可检查接口
@wraps(func) 让包装后的名称、签名和 __wrapped__ 链指回原函数。包装器依然负责正确接收并转发实参。
from functools import wraps
from inspect import signature, unwrap
def require_role(required_role):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
role = kwargs.pop("role")
if role != required_role:
raise PermissionError("role not allowed")
return func(*args, **kwargs)
return wrapper
return decorate
@require_role("admin")
def close_ticket(ticket_id: int, *, reason: str) -> str:
return f"closed {ticket_id}: {reason}"
print(close_ticket.__name__)
print(signature(close_ticket))
print(close_ticket(42, reason="duplicate", role="admin"))
print(unwrap(close_ticket)(7, reason="spam"))close_ticket
(ticket_id: int, *, reason: str) -> str
closed 42: duplicate
closed 7: spaminspect.signature() 默认沿 __wrapped__ 查看原接口,所以输出没有包装器自己消费的 role。这对透明装饰器很合适,但此示例实际扩展了调用协议;生产 API 应明确记录这项差异,或者把身份信息移到上下文而不是伪装成原签名的一部分。
unwrap() 会绕过包装层,因此不能作为授权后的普通调用路径。测试可用它核对原对象,框架和业务代码则应调用装饰后的公开名称。
归约记录并适配旧比较函数
reduce() 用显式初始值构造统计结果,cmp_to_key() 则把现有的三路比较函数交给 sorted()。两个适配器都保留原函数的职责边界。
from functools import cmp_to_key, reduce
def add_record(summary, record):
kinds, total = summary
return kinds | {record["kind"]}, total + record["cents"]
records = [
{"kind": "sale", "cents": 2400},
{"kind": "refund", "cents": -450},
]
kinds, total = reduce(add_record, records, (set(), 0))
priority_order = {"high": 0, "normal": 1}
def compare_ticket(left, right):
left_key = priority_order[left["priority"]], left["id"]
right_key = priority_order[right["priority"]], right["id"]
return (left_key > right_key) - (left_key < right_key)
tickets = [
{"id": 9, "priority": "normal"},
{"id": 5, "priority": "high"},
{"id": 2, "priority": "high"},
]
ordered = sorted(tickets, key=cmp_to_key(compare_ticket))
print(f"kinds: {', '.join(sorted(kinds))}")
print(f"total: {total}")
print("order:", ", ".join(str(ticket["id"]) for ticket in ordered))kinds: refund, sale
total: 1950
order: 2, 5, 9初始值 (set(), 0) 同时定义累积器形状和空输入结果。add_record() 每一步都返回新元组;它没有在调用方传入的容器上偷偷积累状态。
若比较规则可以直接表示为 priority_order[ticket["priority"]], ticket["id"],直接写键函数更短。cmp_to_key() 的价值在于接入已经存在且经过测试的比较函数,而不是把本来简单的键逻辑改写成两两比较。
陷阱
把预填关键字当成不可覆盖配置
修复: 明确默认值与强制值的区别。默认值可以使用 partial();强制值应由具名包装器在转发前拒绝冲突实参,或从调用接口中完全移除该选项。
在类体中用 partial() 代替 partialmethod()
修复: 类定义中的方法特化使用 partialmethod(),并通过实例测试所得属性。若绑定规则已经难以从签名读出,写一个普通方法,显式调用共享实现。
认为 wraps() 会修复包装器行为
修复: 把元数据检查与行为检查分开。对包装器测试原函数的各种参数形状、返回值、异常路径;装饰异步函数时,确保异步包装器在正确边界内执行 await。
省略 reduce() 的空输入契约
修复: 若运算存在真实单位元,显式传入它,并测试空输入。若空输入应是错误,提前给出领域化错误;若意图只是求和、全真、任一为真或连接序列,优先使用对应内置函数或清晰循环。
返回布尔值的比较函数
修复: 比较等价键,并返回 (left_key > right_key) - (left_key < right_key),同时测试小于、相等和大于三种情况。能直接返回键时,省掉比较函数和适配层。
让相等与排序依据不同字段
修复: 让相等与排序从同一规范键导出,对不支持的类型返回 NotImplemented。用相同值、不同值和不相关类型验证所有比较关系,并在分析表明生成方法是热点时再考虑手写全部方法。
深入理解参数绑定
位置参数与 3.14 占位符
没有 Placeholder 时,partial() 只能预填一段领先的位置实参。例如,partial(int, base=2) 预填的是关键字,而 partial(pow, 2) 把第一个位置参数固定为 2。调用时新增的位置实参只会接在已保存位置实参之后。
Python 3.14 的 Placeholder 把「只能固定前缀」扩展为「可以预留任意位置」。若保存实参是 (_, 10, _),调用时的前两个位置实参按顺序替换两个 _,剩余实参继续追加。实参不足以填满全部占位符时,调用抛出 TypeError。
对已有偏函数再次调用 partial() 时,新位置实参会先填充旧占位符。若要保留某个位置继续待填,需要在该位置再次放入 Placeholder。占位符只能出现在位置实参中;把它作为关键字值并不表示待填槽位。
这项功能会让紧凑表达式承载更多位置语义。公共 API 中若读者需要数参数位置才能理解调用,具名包装函数通常更稳妥;它还能给各项校验和错误消息明确命名。
关键字覆盖与可变对象
偏函数保存的关键字像默认配置,而不是不可变策略。调用时同名关键字覆盖保存值,其他关键字继续合并。测试不应只检查默认调用,还应主动传入每个敏感关键字,确认覆盖是允许、拒绝还是被包装器重写。
绑定可变对象不会复制它。若 partial(render, options) 保存了字典,后来修改同一个字典,偏函数下次调用会看到新内容。需要创建时快照就显式复制,并说明浅复制是否足够;需要共享实时配置则把共享所有权写进接口说明。
偏函数公开 func、args 与 keywords,适合诊断已保存配置。不要把直接修改 keywords 字典当作常规重配置协议;建立新的偏函数或提供具名配置对象,能让验证和生命周期更清楚。
偏函数的签名与元数据
inspect.signature() 能根据普通偏函数推导剩余参数,因此调试器通常可以看到配置后的调用形状。偏函数不会自动获得原函数的 __name__ 与文档元数据;它是带 func、args 和 keywords 的独立可调用对象,不是闭包函数。
可以对可写属性的包装对象使用 update_wrapper(),但要理解 __wrapped__ 的含义。若直接把偏函数标成原函数的包装器,inspect.signature() 默认沿链显示原始未绑定签名,可能反而遮住已经预填的参数。公开接口应选择最能描述真实调用协议的元数据,而不是机械复制全部属性。
元数据、描述符与比较生成
wraps() 的边界
Python 3.14 中,默认直接赋值的属性包括 __module__、__name__、__qualname__、__annotations__、__type_params__ 与 __doc__。包装器的 __dict__ 默认会用被包装对象的字典更新,而 __wrapped__ 明确指向被包装对象。
缺少被复制属性的被包装对象不会因此失败,但包装对象若缺少需要更新的属性,update_wrapper() 可能抛出 AttributeError。这就是它能用于多种可调用对象,却不能保证任意对象组合都可写的原因。
__wrapped__ 是检查协议,不是安全边界。它方便签名检查、解包和重新包装,也意味着能拿到包装对象的代码通常可以访问原可调用对象。授权、速率限制或审计不能依赖「调用方找不到原函数」。
装饰器改变实际调用签名时,复制原签名可能误导依赖签名的框架。透明装饰器应完整保持行为;扩展或缩减参数的装饰器应显式公布新接口,并检查框架究竟读取 __wrapped__、__signature__ 还是包装器自身参数。
partialmethod() 的描述符路径
普通函数本身实现描述符协议。partialmethod() 包装函数、classmethod()、staticmethod()、abstractmethod() 或另一个 partialmethod() 时,会把 __get__ 委托给底层描述符,再返回适当的偏函数对象。
底层对象若只是可调用但不是描述符,partialmethod() 会动态创建绑定方法。此时 self 仍插在构造 partialmethod() 时提供的 args 与 keywords 之前。这条顺序规则解释了它为什么不是简单地在类体里调用 partial()。
从类访问与从实例访问得到的对象不同,是描述符的正常表现。测试方法特化时,要从实例取属性并调用;只检查类字典里的 partialmethod 对象无法验证实例绑定是否符合预期。
total_ordering 生成什么
类应定义 __eq__(),并至少定义 __lt__()、__le__()、__gt__()、__ge__() 之一。total_ordering 依据已有排序方法生成缺少的方法,但不会覆盖类或父类已经声明的方法,即使继承来的方法是抽象方法。
装饰器不会生成一致性证明。最稳妥的实现通常先构造一个规范比较键,让 __eq__() 与基础排序方法都比较该键;遇到不支持的类型时返回 NotImplemented,让 Python 有机会尝试反射操作或给出合适结果。
生成的方法会增加一层调用与栈轨迹。不要据此编造通用性能比例;先用应用自己的输入分析热点。若比较确实占据可观成本,再手写所需方法并保持同一规范键和测试矩阵。
归约与顺序契约
initial 决定空输入语义
有 initial 时,归约等价于先把它放在输入序列之前。空输入直接返回 initial,单元素输入也会先调用一次二元函数。没有 initial 时,首个元素成为累积值;空输入没有可用首项,因此抛出 TypeError。
初始值必须符合累积器契约,而不只是看起来像一个默认值。把 0 传给累积字典的函数会在第一步失败;把共享列表作为初始值并原地修改,可能让状态跨调用泄漏。优先使用不可变初始值,或在每次调用前创建新的可变容器。
Python 3.14 支持 reduce(function, iterable, initial=value)。如果库需要兼容 3.13 及更早版本,应继续传第三个位置实参,或者明确提高最低版本;生成代码不能只因为目标文档是 3.14 就假定部署环境已经升级。
itertools.accumulate() 与 reduce() 的观察面不同。前者逐步产出每个中间累积值,后者只返回最终值。需要进度、审计或提前停止时,显式循环往往比把副作用塞进二元归约函数更清楚。
比较函数必须形成稳定关系
三路比较函数对同一对相等值应返回零,交换实参后符号应反转,传递关系也应成立。依赖当前时间、随机数或调用次数的比较器会破坏这些性质;排序结果可能不稳定,问题也很难用单个样例复现。
cmp_to_key() 创建的包装对象通过富比较方法调用原比较器。它适合 locale.strcoll 一类现成协议或迁移中的旧接口。新代码若能计算稳定键,直接键函数通常让相等、优先级与后备排序字段更容易审查。
排序键相同不代表对象在业务上相等。稳定排序会保留相同键元素的原相对次序,但 total_ordering 定义的是对象比较语义。不要因为两者都参与排序,就把键函数与 __eq__() 的领域契约混为一谈。
延伸阅读
4个问题 · 2 道输出预测题 · 1 道找错题