functools模块 — 高阶函数与操作

Python标准库精讲专题 · 函数式编程篇 · 掌握高阶函数工具

专题:Python标准库精讲系统学习

关键词:Python, 标准库, functools, reduce, partial, lru_cache, cache, wraps, singledispatch, total_ordering

一、functools 模块概述

functools 是 Python 标准库中专门为高阶函数(higher-order functions)提供的工具模块。所谓高阶函数,是指那些以函数作为参数或返回值的函数。functools 模块汇集了一系列用于操作和增强函数功能的实用工具,是函数式编程风格的核心支撑模块。

该模块的主要工具可分为五大类:归约操作(reduce)、偏函数(partial)、缓存装饰器(lru_cache / cache / cached_property)、函数元信息维护(wraps / update_wrapper)、以及泛函数机制(singledispatch / singledispatchmethod)。此外还包含排序辅助工具(cmp_to_key)和比较方法自动生成器(total_ordering)。

functools 在 Python 3 中得到持续增强。Python 3.9 引入了 cache 装饰器作为 lru_cache 的无上限简化版本;Python 3.8 引入了 singledispatchmethod 用于方法级别的单分派;Python 3.10 为 singledispatch 增加了 union 类型支持。理解 functools 是掌握 Python 函数式编程进阶技巧的关键一步。

核心要点:functools 提供的不是"新功能",而是"函数操作的语法糖"——它让你用更少的代码表达更强的函数逻辑,是减少重复、提升抽象层次的标准工具集。

二、reduce 归约

2.1 reduce 函数的基本用法

reduce 函数源自函数式编程中经典的"fold"(折叠)操作,它将一个二元操作函数累积地应用到序列的元素上,从而将序列归约为单个值。其函数签名为 reduce(function, iterable[, initial])。

工作流程:首先从可迭代对象中取出前两个元素(若有初始值则取初始值与第一个元素),应用二元函数得到中间结果;然后将中间结果与下一个元素继续应用该函数,直到遍历完所有元素,返回最终值。

from functools import reduce # 计算 1+2+3+4+5 = 15 result = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]) print(result) # 15 # 计算 5! = 120 factorial = reduce(lambda x, y: x * y, range(1, 6)) print(factorial) # 120 # 列表扁平化:[[1,2], [3,4], [5]] -> [1,2,3,4,5] flattened = reduce(lambda acc, item: acc + item, [[1, 2], [3, 4], [5]], []) print(flattened) # [1, 2, 3, 4, 5]

2.2 初始值(initial)的意义

initial 参数提供归约的起始值。当提供了 initial,reduce 从 initial 和序列的第一个元素开始操作。初始值的核心价值有两方面:其一,当序列为空时,直接返回 initial 而不会抛出 TypeError;其二,对非交换、非结合的操作而言,初始值决定了计算顺序的基准。

例如计算乘积时提供初始值 1,计算列表拼接时提供空列表 [] 作为初始值,都可以保证即使输入为空也能返回有意义的结果。

# 提供一个初始值,空序列也不报错 safe_sum = reduce(lambda x, y: x + y, [], 0) print(safe_sum) # 0 # 不提供初始值时空序列抛出 TypeError try: reduce(lambda x, y: x + y, []) except TypeError as e: print(f"无初始值+空序列: {e}") # 用初始值 1 计算连乘 product = reduce(lambda x, y: x * y, [2, 3, 4], 1) print(product) # 24

2.3 右折叠(reduce 的左倾性)

Python 的 reduce 本质上是"左折叠"(left fold),即从左向右累积。在某些场景下需要"右折叠"(right fold),即从右向左操作。实现右折叠的最简单方式是对序列取反后再应用 reduce,或者使用自定义的右折叠函数。

左折叠与右折叠的区别在减法、除法等非结合操作中尤为明显。例如 reduce(sub, [1,2,3]) 等价于 (1-2)-3 = -4,而右折叠应当等价于 1-(2-3) = 2。

# 左折叠: (((1 - 2) - 3) - 4) = -8 left_fold = reduce(lambda x, y: x - y, [1, 2, 3, 4]) print(f"左折叠: {left_fold}") # -8 # 右折叠: (1 - (2 - (3 - 4))) = -2 right_fold = reduce(lambda x, y: y - x, reversed([1, 2, 3, 4])) print(f"右折叠: {right_fold}") # -2

2.4 reduce 与 sum / any / all 的对比

Python 为常见的归约操作提供了专用函数,这些函数在可读性和性能上通常优于 reduce。推荐的使用原则是:有专用函数的优先使用专用函数,没有专用函数的再考虑 reduce。

操作专用函数reduce 等价写法推荐
求和sum(iterable, start=0)reduce(add, iterable, 0)sum 更优(C 实现,更快)
逻辑或any(iterable)reduce(or_, iterable, False)any 更优(短路求值)
逻辑与all(iterable)reduce(and_, iterable, True)all 更优(短路求值)
最大值max(iterable)reduce(max, iterable)max 更优
最小值min(iterable)reduce(min, iterable)min 更优
连乘math.prod (3.8+)reduce(mul, iterable, 1)math.prod 更优

reduce 的独特价值在于那些没有专用函数的归约操作,例如求最大公约数、字符串交替拼接、嵌套字典合并等自定义累积逻辑。

三、partial 偏函数

3.1 偏函数的概念与使用

partial 用于"冻结"一个函数的某些参数(位置参数或关键字参数),从而创建一个参数更少的新函数。这在函数式编程中称为"偏应用"(partial application),是柯里化(currying)的一种简化形式。

partial 函数签名为 functools.partial(func, /, *args, **keywords),返回一个可调用的 partial 对象。当调用这个 partial 对象时,它会将预设的参数和调用时传入的参数合并后转发给原始函数。

from functools import partial def power(base, exponent): return base ** exponent # 创建固定指数的偏函数 square = partial(power, exponent=2) cube = partial(power, exponent=3) print(square(5)) # 25 print(cube(5)) # 125 print(power(2, 10)) # 1024(原始函数不受影响) # 固定 base 也可以 two_power = partial(power, 2) # 固定 base=2 print(two_power(10)) # 1024

3.2 partial 对象的内部属性

partial 返回的对象具有三个只读属性,分别揭示了偏函数绑定的参数信息:

from functools import partial def log(level, message, timestamp=None): parts = [f"[{level}]", message] if timestamp: parts.append(f"({timestamp})") return " ".join(parts) info_log = partial(log, "INFO", timestamp="2026-05-05") print(info_log.func) # print(info_log.args) # ('INFO',) print(info_log.keywords) # {'timestamp': '2026-05-05'} # 调用时还可以继续传参 print(info_log("系统启动")) # [INFO] 系统启动 (2026-05-05) # 调用时传入的 message 会放在 args 的已有值之后 # 相当于 log('INFO', '系统启动', timestamp='2026-05-05')

3.3 偏函数的实际应用场景

偏函数在以下几个场景中特别有用:

from functools import partial # 场景一:替代简单的 lambda # 不推荐: lambda x: pow(x, 2) # 推荐: square = partial(pow, 2) # pow(2, x) 而不是 pow(x, 2)! # 注意顺序:partial(pow, 2) 相当于固定第一个参数为 2 # 所以 pow(2, x) 其实是 2**x,这不是平方而是 2 的幂 # 正确写法:partial(pow, exp=2) 或自定义函数 # 场景二:多进程参数绑定 from multiprocessing import Pool def process_file(file_path, encoding="utf-8"): # 处理文件的逻辑 return f"处理 {file_path} (编码: {encoding})" files = ["a.txt", "b.txt", "c.txt"] process_with_encoding = partial(process_file, encoding="gbk") # with Pool(4) as pool: # results = pool.map(process_with_encoding, files) # 场景三:GUI 回调绑定 # button.on_click(partial(handler, user_id=42))

四、缓存装饰器

4.1 lru_cache 最近最少使用缓存

lru_cache 是 functools 中最常用的缓存装饰器,它通过"最近最少使用"(Least Recently Used)淘汰策略自动缓存函数的返回值。对于计算密集型或 I/O 密集型的纯函数(相同输入总是返回相同输出),lru_cache 可以显著提升性能。

其函数签名为 functools.lru_cache(maxsize=128, typed=False)。maxsize 指定缓存的最大条目数(设为 None 表示无限制),typed 为 True 时区分不同参数类型(如 1 和 1.0 视为不同调用)。

from functools import lru_cache import time # 未缓存版本 def fib_slow(n): if n < 2: return n return fib_slow(n-1) + fib_slow(n-2) # 缓存版本 @lru_cache(maxsize=128) def fib_fast(n): if n < 2: return n return fib_fast(n-1) + fib_fast(n-2) # 性能对比 start = time.time() fib_slow(35) print(f"未缓存: {time.time() - start:.3f}s") # 约 2~4 秒 start = time.time() fib_fast(35) print(f"已缓存: {time.time() - start:.3f}s") # 毫秒级 print(fib_fast.cache_info()) # CacheInfo(hits=32, misses=36, maxsize=128, currsize=36)

lru_cache 对象提供了几个有用的方法:cache_info() 返回命中次数、未命中次数、最大容量和当前大小;cache_clear() 清空缓存;cache_parameters() 返回配置参数。

4.2 cache 无限缓存(Python 3.9+)

cache 是 Python 3.9 引入的简化版缓存装饰器,等价于 lru_cache(maxsize=None),即不限制缓存大小。当你确定缓存数量可控或希望尽可能多地缓存结果时,cache 比 lru_cache 更简洁。

使用 cache 时所有返回值都会被永久缓存(直到进程重启或手动清除),因此不适合参数组合无限多的函数(如接受用户输入的函数),否则会导致内存泄漏。

from functools import cache @cache def expensive_computation(n): """模拟一个耗时计算""" print(f"正在计算 {n}...") return n * n print(expensive_computation(10)) # 计算并缓存 print(expensive_computation(10)) # 直接返回缓存,不会输出"正在计算" print(expensive_computation(20)) # 计算并缓存 # cache 等价于 # @lru_cache(maxsize=None) # def expensive_computation(n): ...

4.3 cached_property 属性缓存

cached_property 是 Python 3.8 引入的装饰器,它将一个类方法转换为惰性求值的缓存属性。与 property 不同,cached_property 只在首次访问时调用被装饰的方法,之后将结果缓存起来,后续访问直接返回缓存值而不重新计算。

与 property 的关键区别:cached_property 的结果会被存储在实例的 __dict__ 中(键名为属性名),这意味着它只能用于没有同名实例属性的场景,且缓存值可以被直接删除以触发重新计算。

from functools import cached_property import time class DataProcessor: def __init__(self, data): self.data = data @cached_property def processed(self): """昂贵的计算,只执行一次""" print("执行复杂数据处理...") time.sleep(1) # 模拟耗时 return [x * 2 for x in self.data] @property def summary(self): """普通 property,每次调用都重新计算""" print("重新生成摘要...") return f"数据共 {len(self.data)} 条" dp = DataProcessor([1, 2, 3, 4, 5]) # 第一次访问:触发计算 print(dp.processed) # 输出 "[2, 4, 6, 8, 10]" 并打印"执行" # 第二次访问:立即返回缓存 print(dp.processed) # 只输出 "[2, 4, 6, 8, 10]" # 删除缓存可触发重新计算 del dp.processed print(dp.processed) # 再次打印"执行"并计算

使用建议:lru_cache 适合有明确上限的场景(如递归、固定参数集);cache 适合参数组合有限且确定不会无限增长的场景;cached_property 适合类中那些计算一次后不再变化的派生属性。对于可变对象或依赖外部状态的函数,切勿使用任何缓存装饰器。

五、wraps 与更新

5.1 装饰器对函数元信息的破坏

Python 装饰器的本质是将原函数替换为包装函数(wrapper),这会导致原函数的元信息丢失——包括 __name__、__doc__、__module__、__annotations__ 以及 __dict__ 等属性全部被 wrapper 函数覆盖。这对调试、文档生成、序列化等场景造成困扰。

def my_decorator(func): def wrapper(*args, **kwargs): """包装函数的文档""" print("调用前") result = func(*args, **kwargs) print("调用后") return result return wrapper @my_decorator def greet(name): """向指定的人打招呼""" print(f"你好, {name}!") print(greet.__name__) # wrapper(丢失了原名) print(greet.__doc__) # 包装函数的文档(丢失了原文档)

5.2 wraps 保留原函数元信息

wraps 是 functools 提供的一个装饰器,用于在定义 wrapper 函数时自动将原函数的元信息复制过来。它是 update_wrapper 的便捷装饰器版本,本质上是 partial(update_wrapper, wrapped=func, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)。

from functools import wraps def my_decorator(func): @wraps(func) # 关键:将 func 的元信息复制到 wrapper def wrapper(*args, **kwargs): """包装函数的文档""" print("调用前") result = func(*args, **kwargs) print("调用后") return result return wrapper @my_decorator def greet(name): """向指定的人打招呼""" print(f"你好, {name}!") print(greet.__name__) # greet(原函数名保留) print(greet.__doc__) # 向指定的人打招呼(原文档保留)

5.3 update_wrapper 与 WRAPPER_ASSIGNMENTS

update_wrapper 是 wraps 的底层实现,它直接将 wrapped 函数的属性复制到 wrapper 函数。你可以通过修改 WRAPPER_ASSIGNMENTS 和 WRAPPER_UPDATES 来控制复制哪些属性。

默认情况下,WRAPPER_ASSIGNMENTS 包含 __module__、__name__、__qualname__、__annotations__、__doc__ 五个属性;WRAPPER_UPDATES 包含 __dict__,表示更新 wrapper 的 __dict__(合并而非覆盖)。

from functools import update_wrapper, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES print("默认复制属性:", WRAPPER_ASSIGNMENTS) # ('__module__', '__name__', '__qualname__', '__annotations__', '__doc__') print("默认更新属性:", WRAPPER_UPDATES) # ('__dict__',) # 手动使用 update_wrapper def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return update_wrapper(wrapper, func) # 自定义:只复制 name 和 doc def minimal_wrapper(wrapper, wrapped): wrapper.__name__ = wrapped.__name__ wrapper.__doc__ = wrapped.__doc__ return wrapper

最佳实践:在编写自定义装饰器时始终使用 @wraps(func)。这不仅有助于调试(堆栈信息显示正确的函数名),还能保证文档字符串自动继承,是合格装饰器的基本功。

六、单分派泛函数

6.1 singledispatch 的概念与意义

singledispatch 为 Python 提供了"单分派泛函数"(Single-Dispatch Generic Function)机制。它允许你根据第一个参数的类型来动态选择不同的函数实现,类似于某些语言中的方法重载(overloading),但 Python 的动态类型系统需要运行时派发。

所谓"单分派"(single dispatch)是指只根据一个参数(通常是第一个参数)的类型进行分派,与之相对的是"多分派"(multiple dispatch),后者根据多个参数的类型进行分派。

from functools import singledispatch @singledispatch def process(value): """处理各种类型的数据""" raise TypeError(f"不支持的类型: {type(value)}") @process.register(int) def _(value): """处理整数""" return f"整数: {value} ({value:#x})" @process.register(str) def _(value): """处理字符串""" return f"字符串: {value!r} (长度 {len(value)})" @process.register(list) @process.register(tuple) def _(value): """处理序列""" return f"序列: 共 {len(value)} 个元素, 首项={value[0] if value else None}" print(process(42)) # 整数: 42 (0x2a) print(process("hello")) # 字符串: 'hello' (长度 5) print(process([1, 2])) # 序列: 共 2 个元素, 首项=1

6.2 register 注册重载

register 是 singledispatch 对象的核心方法,用于为特定类型注册专门的函数实现。它可以作为装饰器使用(如上例),也可以直接调用 register(type, func) 来注册已经定义的函数。

注册函数时,函数名可以任意(通常使用 _ 表示这些函数不打算被外部直接调用)。多个类型可以注册到同一个实现(如上例中的 list 和 tuple 共用一个实现)。

from functools import singledispatch @singledispatch def format_output(value): return f"未知类型: {value}" # 方式一:装饰器注册 @format_output.register(float) def _(value): return f"浮点数: {value:.2f}" # 方式二:直接调用 register 注册已定义的函数 def format_bool(value): return f"布尔值: {'真' if value else '假'}" format_output.register(bool, format_bool) # 方式三:为 None 注册 format_output.register(type(None), lambda _: "空值 None") print(format_output(3.14159)) # 浮点数: 3.14 print(format_output(True)) # 布尔值: 真 print(format_output(None)) # 空值 None

6.3 dispatch 查找与类型层次

singledispatch 对象还提供了 dispatch(cls) 方法,用于查看给定类型会分派到哪个具体的函数实现。这在调试和测试中非常有用。

类型分派的查找遵循 MRO(方法解析顺序,Method Resolution Order)规则。如果某个类型没有直接注册的处理函数,singledispatch 会沿着 MRO 链向上查找最接近的父类型注册的实现。如果最终也没有找到匹配的,则调用 base 函数(默认行为)。

from functools import singledispatch @singledispatch def handler(value): return "默认处理" @handler.register(int) def _(value): return "整数处理" # dispatch 查看分派目标 print(handler.dispatch(int)) # print(handler.dispatch(str)) # (默认) # 类型层次继承 class Animal: pass class Dog(Animal): pass class Cat(Animal): pass @singledispatch def speak(animal): return "未知动物" @speak.register(Animal) def _(animal): return "某种动物" @speak.register(Dog) def _(animal): return "汪汪!" # Cat 没有注册,沿着 MRO 找到 Animal print(speak(Dog())) # 汪汪! print(speak(Cat())) # 某种动物(沿 MRO 找到 Animal) # 查看所有注册类型 print(speak.registry) # {: , : , : }

6.4 singledispatchmethod 方法级单分派

singledispatchmethod 是 Python 3.8 引入的装饰器,将 singledispatch 的能力扩展到类方法上。与 singledispatch 不同,它可以正确处理绑定方法调用,即 self 参数不会被纳入分派,分派的是第一个非 self 参数。

from functools import singledispatchmethod class PrettyPrinter: @singledispatchmethod def display(self, value): print(f"默认: {value}") @display.register(int) def _(self, value): print(f"整数: {value:,}") @display.register(str) def _(self, value): print(f"字符串: 「{value}」") @display.register(list) def _(self, value): print(f"列表 [{len(value)}项]: {value[:3]}{'...' if len(value) > 3 else ''}") pp = PrettyPrinter() pp.display(1234567) # 整数: 1,234,567 pp.display("你好世界") # 字符串: 「你好世界」 pp.display([1, 2, 3, 4]) # 列表 [4项]: [1, 2, 3, 4]...

注意:singledispatchmethod 的 register 装饰器要求注册的函数接受 self 作为第一个参数,这与普通的 singledispatch 不同。从 Python 3.10 开始,register 支持使用类型注解语法(format_output.register 与 format_output.register(int) 两种形式均可)。

七、比较与排序

7.1 total_ordering 自动生成比较方法

total_ordering 是一个类装饰器,它根据你定义的 __eq__ 和另外一个比较方法(__lt__、__le__、__gt__、__ge__ 中的任意一个),自动生成其余的三个比较方法。这避免了手动编写所有六个比较方法(__eq__、__ne__、__lt__、__le__、__gt__、__ge__)的重复劳动。

其实现原理很简单:利用代数关系进行推导。例如,有了 __eq__ 和 __lt__,就可以推导出 __le__(a <= b 等价于 a < b or a == b)、__gt__(a > b 等价于 b < a)、__ge__(a >= b 等价于 not a < b)和 __ne__(a != b 等价于 not a == b)。

from functools import total_ordering @total_ordering class Student: def __init__(self, name, grade): self.name = name self.grade = grade def __eq__(self, other): if not isinstance(other, Student): return NotImplemented return self.grade == other.grade def __lt__(self, other): if not isinstance(other, Student): return NotImplemented return self.grade < other.grade def __repr__(self): return f"{self.name}({self.grade})" # 现在自动拥有了 __le__, __gt__, __ge__, __ne__ s1 = Student("Alice", 85) s2 = Student("Bob", 92) s3 = Student("Charlie", 85) print(s1 < s2) # True(自定义) print(s1 <= s2) # True(自动生成) print(s2 > s1) # True(自动生成) print(s2 >= s3) # True(自动生成) print(s1 != s2) # True(自动生成) print(s1 == s3) # True(自定义)

total_ordering 的性能开销:自动生成的方法是通过元编程实现的,在频繁比较的场景下(如排序数百万个对象),手动实现所有六个方法的性能会更好。但对于大多数日常场景,性能差异可以忽略不计。

7.2 cmp_to_key 自定义排序

cmp_to_key 是一个将旧式比较函数(cmp function)转换为键函数(key function)的适配器。在 Python 3 中,sorted()、list.sort()、max()、min() 等函数不再接受 cmp 参数,只接受 key 参数。cmp_to_key 帮助那些已经写好了比较函数的代码平滑迁移到 Python 3。

比较函数的签名是 cmp(a, b) -> int,返回负数表示 a < b,正数表示 a > b,零表示 a == b。cmp_to_key 将这种比较函数封装为一个实现了 __lt__ 等比较方法的可比较对象。

from functools import cmp_to_key # 定义一个比较函数:按字符串长度降序,长度相同时按字母序升序 def custom_cmp(a, b): if len(a) != len(b): return len(b) - len(a) # 长度降序 if a < b: return -1 # 字母序升序 if a > b: return 1 return 0 words = ["apple", "pear", "banana", "kiwi", "grape", "fig"] sorted_words = sorted(words, key=cmp_to_key(custom_cmp)) print(sorted_words) # ['banana', 'apple', 'grape', 'pear', 'kiwi', 'fig'] # banana(6)>apple(5)/grape(5)/pear(4)/kiwi(4)/fig(3)

最佳实践:在 Python 3 中,优先使用 key 参数结合 lambda 或 operator 模块(如 operator.attrgetter)来实现自定义排序。cmp_to_key 主要用于兼容遗留代码,新代码应直接使用 key 表达式。

八、核心总结

functools 模块工具一览

reduce —— 归约折叠,将序列累积为单个值。适用于没有专用函数的自定义累积操作,但应优先使用 sum/any/all/max/min 等专用函数。

partial —— 偏函数,冻结函数的某些参数生成新函数。适用于回调适配、API 封装、参数绑定等场景,是替代简单 lambda 的更优雅方案。

lru_cache / cache —— 缓存装饰器,自动缓存函数返回值。lru_cache 有容量上限(LRU 淘汰),cache 无上限(3.9+),两者都要求被装饰函数是纯函数(无副作用、不依赖外部状态)。

cached_property —— 属性缓存,将类方法变为只计算一次的缓存属性。适用于计算成本高但结果不变的派生属性。

wraps / update_wrapper —— 函数元信息维护。在自定义装饰器中始终使用 @wraps(func) 来保留原函数的 __name__、__doc__ 等重要属性。

singledispatch / singledispatchmethod —— 单分派泛函数,根据参数类型动态分派到不同的函数实现。适用于类型相关的多分支处理逻辑,比长长的 if-elif-else 更可维护。

total_ordering —— 比较方法自动生成。定义了 __eq__ 和 __lt__(或其他一个)后自动补全所有比较方法,减少样板代码。

cmp_to_key —— 比较函数适配器。将 Python 2 风格的 cmp 函数转换为 key 函数,主要用于向下兼容。

functools 模块的哲学可以概括为一句话:让函数操作函数。它提供的每个工具都在尝试减少重复代码、提升抽象层次、让代码更具声明性。掌握 functools,意味着你不再仅仅编写"函数",而是开始编写"操作函数的函数"——这是函数式编程思维成熟的重要标志。

实际项目中,建议从最常用的几项开始:用 @wraps 规范装饰器编写、用 @lru_cache 优化纯函数性能、用 partial 简化重复参数传递。逐步深入后再掌握 singledispatch 和 total_ordering 等更高级的特性。