在 Python 的标准库中,有一个专门为函数式编程(Functional Programming)设计的模块——functools。它提供了一系列高阶函数(Higher-order functions),即“作用于函数”或“返回函数”的函数。
熟练掌握 functools,不仅能让你的代码更加简洁、优雅,还能显著提升程序性能。本文将通过实际代码示例,详细解读该模块中最常用的几个工具。
1. @lru_cache:性能加速器(缓存)
这是 functools 中最受欢迎的功能之一。全称为 "Least Recently Used Cache"(最近最少使用缓存)。
作用:它能自动把函数的输入和输出结果缓存起来。当下次以相同的参数调用该函数时,直接从内存返回结果,而不再重新计算。非常适合耗时的计算或递归操作。
示例:斐波那契数列
如果不加缓存,递归计算斐波那契数列会有大量的重复计算,效率极低。
from functools import lru_cacheimport time# maxsize=None 表示缓存大小无限,默认为 128@lru_cache(maxsize=None)deffib(n):if n < 2:return nreturn fib(n-1) + fib(n-2)start = time.time()print(f"结果: {fib(35)}") # 瞬间完成print(f"耗时: {time.time() - start:.4f}秒")# 查看缓存命中情况print(fib.cache_info())# 输出示例: CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)
注意:
- 自 Python 3.9 起,可以使用
@functools.cache,它是 @lru_cache(maxsize=None) 的简化版。 - 被缓存函数的参数必须是可哈希的(hashable),例如列表(list)不能作为参数,但元组(tuple)可以。
2. partial:偏函数(参数冻结)
作用:基于一个已有的函数,固定(冻结)其中一部分参数,生成一个新的函数。这样在调用新函数时,只需要传递剩余的参数即可。
场景:当你需要频繁调用某个函数,且其中某些参数总是固定值时,使用 partial 可以简化代码。
示例:定制转换函数
from functools import partial# int() 函数默认按十进制转换,但它接受一个 base 参数print(int('1010', base=2)) # 输出 10# 使用 partial 固定 base=2,创建一个专门处理二进制转换的函数int2 = partial(int, base=2)print(int2('1010')) # 输出 10print(int2('1111')) # 输出 15
3. @wraps:装饰器的伴侣
作用:在编写装饰器(Decorator)时,使用 @wraps 可以保留原函数的元数据(如 __name__,__doc__,参数签名等)。
问题:如果不使用 @wraps,被装饰后的函数其实变成了装饰器内部的 wrapper 函数,这会导致调试困难,且无法获取原函数的文档字符串。
示例:标准的装饰器写法
from functools import wrapsdefmy_logger(func): @wraps(func) # 关键行:保留原函数 func 的元数据defwrapper(*args, **kwargs): print(f"正在执行函数: {func.__name__}")return func(*args, **kwargs)return wrapper@my_loggerdefadd(x, y):"""这是一个加法函数"""return x + yprint(add(1, 2))print(f"函数名: {add.__name__}") # 输出: add (如果没有 @wraps,这里会输出 wrapper)print(f"文档: {add.__doc__}") # 输出: 这是一个加法函数
4. reduce:累积计算
作用:将一个二元函数(接收两个参数)作用于序列的元素,从左到右进行累积运算,最终合并为一个值。
注:在 Python 2 中它是全局函数,Python 3 中移到了 functools 模块。
示例:累乘(阶乘)
from functools import reducedata = [1, 2, 3, 4, 5]# 计算过程:((((1*2)*3)*4)*5)result = reduce(lambda x, y: x * y, data)print(result) # 输出 120
虽然 sum() 可以做加法,但在处理更复杂的累积逻辑(如合并字典、路径拼接)时,reduce 非常强大。
5. @singledispatch:单分发泛型函数
作用:它允许你根据第一个参数的类型,让同一个函数表现出不同的行为。这类似于其他语言中的函数重载(Overloading),但更加 Pythonic,避免了大量的 if isinstance(...) 判断。
示例:根据数据类型格式化输出
from functools import singledispatch@singledispatchdefformat_data(data):"""默认处理逻辑""" print(f"未知类型: {data}")@format_data.register(str)def_(data): print(f"处理字符串: {data.upper()}")@format_data.register(list)def_(data): print(f"处理列表,长度为: {len(data)}")@format_data.register(int)def_(data): print(f"处理整数: {data * 10}")# 调用同一个函数,行为不同format_data("hello") # 处理字符串: HELLOformat_data([1, 2, 3])# 处理列表,长度为: 3format_data(42) # 处理整数: 420format_data(1.5) # 未知类型: 1.5
6. @cached_property:惰性属性(Python 3.8+)
作用:将类的方法转换为属性,并且只计算一次。计算结果会被存储在实例的 __dict__ 中,之后的调用直接读取属性值。
场景:适用于计算成本高昂(如读取大文件、复杂运算),且在实例生命周期内结果不变的属性。
示例:数据集加载
from functools import cached_propertyimport timeclassDataSet:def__init__(self, filename): self.filename = filename @cached_propertydefdata(self): print("正在加载巨型数据...") time.sleep(2) # 模拟耗时操作return {"id": 1, "content": "Big Data"}ds = DataSet("file.csv")print("对象已创建")# 第一次访问,会执行耗时操作print(ds.data) # 第二次访问,直接从内存获取,无延迟print(ds.data)
7. @total_ordering:补全比较方法
作用:在定义类时,如果你定义了 __eq__ 和以下四个比较方法之一(__lt__, __le__, __gt__, __ge__),使用该装饰器可以自动补全剩下的比较方法。
场景:简化自定义对象的排序逻辑代码。
示例:学生成绩比较
from functools import total_ordering@total_orderingclassStudent:def__init__(self, name, score): self.name = name self.score = scoredef__eq__(self, other):return self.score == other.score# 只需要定义小于 (__lt__),total_ordering 会自动补全 >、<=、>=def__lt__(self, other):return self.score < other.scores1 = Student("Alice", 85)s2 = Student("Bob", 90)print(s1 < s2) # True (手动定义的)print(s1 > s2) # False (自动补全的)print(s1 <= s2) # True (自动补全的)
总结
functools 是 Python 中极具生产力的模块。
- 想优化性能? 用
@lru_cache 或 @cached_property。 - 想写更清晰的类型判断逻辑? 用
@singledispatch。
掌握这些工具,能让你从繁琐的样板代码中解放出来,专注于核心逻辑的实现。