Python functools模块:偏函数缓存与装饰器实践
functools 是 Python 标准库中处理函数和可调用对象的高阶工具集合。原文列出的核心导入包括 reduce、partial、lru_cache、wraps、total_ordering、singledispatch、cmp_to_key、cached_property。它常用于缓存优化、偏函数、装饰器元信息保留、函数重载和比较方法补全。一、partial:预填充参数生成新函数
partial(func, *args, **kwargs) 可以固定部分参数,创建一个新的可调用对象,相当于 Python 中函数柯里化的一种实现。基础示例如下:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(10))# 100
print(cube(10)) # 1000
在实际回调场景中,可以用 partial 固定 logger 名称,再继续固定 event_type 和时间戳,减少重复传参:
from functools import partial
import time
def log_event(logger, event_type, message, timestamp):
print(f'[{timestamp}] {event_type}: {message} (logger={logger})')
app_log = partial(log_event, 'app')
app_log_error = partial(app_log, 'ERROR', timestamp=time.time())
app_log_error('数据库连接失败')
# ERROR: 数据库连接失败 (logger=app)
数据处理管道也适合用 partial 生成专用转换函数:
from functools import partial
def transform(data, multiplier, offset, round_digits=2):
return round(data * multiplier + offset, round_digits)
c_to_f = partial(transform, multiplier=9/5, offset=32, round_digits=1)
f_to_c = partial(transform, multiplier=5/9, offset=-32*5/9, round_digits=1)
print(c_to_f(0)) # 32.0
print(c_to_f(100))# 212.0
print(f_to_c(32)) # 0.0
二、lru_cache 与 cache:缓存函数结果
lru_cache 用于最近最少使用缓存;cache 是 Python 3.9+ 提供的无限制缓存,等价于 lru_cache(maxsize=None)。用 lru_cache 改写递归函数,可以避免重复子问题计算:
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
start = time.perf_counter()
print(fibonacci(35))
print(f'首次: {time.perf_counter() - start:.4f}秒')
start = time.perf_counter()
print(fibonacci(35))
print(f'缓存: {time.perf_counter() - start:.6f}秒')
print(fibonacci.cache_info())
# CacheInfo(hits=34, misses=36, maxsize=128, currsize=36)
fibonacci.cache_clear()
使用 lru_cache 时要注意:被缓存函数的参数必须可哈希;lru_cache 装饰器不能直接用于方法,否则可能导致内存泄漏。数据库查询缓存是常见场景:
from functools import lru_cache
@lru_cache(maxsize=256)
def get_user_by_id(user_id):
print(f'查询数据库: user_id={user_id}')
return {'id': user_id, 'name': f'用户{user_id}'}
print(get_user_by_id(1))# 查询数据库
print(get_user_by_id(1))# 缓存命中,不查数据库
三、wraps:保留被装饰函数的元信息
写装饰器时,如果不使用 wraps,原函数的 __name__、__doc__ 等元信息会被 wrapper 覆盖:
from functools import wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
'''这是wrapper的文档'''
return func(*args, **kwargs)
return wrapper
@bad_decorator
def greet(name):
'''向用户打招呼'''
return f'Hello, {name}!'
print(greet.__name__)# wrapper
print(greet.__doc__) # 这是wrapper的文档
加上 @wraps(func) 后,元信息能够正确保留:
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
'''这是wrapper的文档'''
return func(*args, **kwargs)
return wrapper
@good_decorator
def greet_v2(name):
'''向用户打招呼'''
return f'Hello, {name}!'
print(greet_v2.__name__)# greet_v2
print(greet_v2.__doc__) # 向用户打招呼
因此,@wraps 基本是编写装饰器时的标配,不加会增加调试和文档阅读成本。
四、singledispatch:按类型分派的函数重载
singledispatch 可以根据第一个参数类型选择不同实现,类似单分派函数重载:
from functools import singledispatch
@singledispatch
def format_output(data):
return str(data)
@format_output.register(list)
def _(data):
return '[' + ', '.join(format_output(item) for item in data) + ']'
@format_output.register(dict)
def _(data):
items =
return '{' + ', '.join(items) + '}'
@format_output.register(int)
def _(data):
return f'{data:,}'
print(format_output(1234567)) # 1,234,567
print(format_output(])) # ]
print(format_output({'name': '张三', 'age': 25}))# {name: 张三, age: 25}
五、cached_property:只计算一次的惰性属性
cached_property 把耗时计算变成实例属性风格访问,第一次访问时计算并缓存,后续访问直接取缓存:
from functools import cached_property
import time
class DataAnalyzer:
def __init__(self, data):
self.data = data
@cached_property
def statistics(self):
print('正在计算统计数据...')
time.sleep(0.5)
return {
'sum': sum(self.data),
'avg': sum(self.data) / len(self.data),
'min': min(self.data),
'max': max(self.data),
}
analyzer = DataAnalyzer()
print(analyzer.statistics)# 计算,并打印“正在计算...”
print(analyzer.statistics)# 缓存命中,不再打印
它与 property 的区别在于:cached_property 像实例属性一样直接访问,同时只计算一次。
六、total_ordering:自动补全比较方法
total_ordering 适合实现版本号等可比较对象。只要定义 __eq__ 和 __lt__,其余比较方法会自动生成:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other):
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
def __repr__(self):
return f'v{self.major}.{self.minor}.{self.patch}'
v1 = Version(1, 2, 3)
v2 = Version(2, 0, 0)
print(v1 < v2) # True
print(v1 <= v2)# True,自动生成
print(v1 >= v2)# False,自动生成
print(v1 != v2)# True,自动生成
七、总结:functools 工具选择建议
原文章给出的常用工具可以归纳为:partial 固定参数并创建新函数;lru_cache 自动缓存,适合加速递归和重复调用;wraps 保留装饰函数的元信息;cached_property 惰性计算且只算一次;singledispatch 根据类型选择实现;total_ordering 补全所有比较方法。实践时可以按场景选择:写装饰器用 wraps,慢递归用 lru_cache,重复参数用 partial。
Re: Python functools模块:偏函数缓存与装饰器实践
感谢分享,整理得很清楚。partial 那几个例子很直观,尤其是日志和温度转换,能看出怎么把通用函数变成专用函数。lru_cache 的 Fibonacci 对比和 cache_info 输出也很有参考价值,参数必须可哈希、谨慎用在实例方法上这两点确实容易踩坑。wraps 的 bad 和 good 对比把元信息丢失问题讲明白了,期待后面 singledispatch、total_ordering、cmp_to_key、cached_property 的实践部分。想请教一下,用 lru_cache 做数据库查询缓存时,如果数据会更新,你一般是怎么处理缓存失效的?是设置短 maxsize、手动 cache_clear,还是干脆用别的方案?Re: Python functools模块:偏函数缓存与装饰器实践
整理得挺系统,partial 的例子很实用,尤其是把 logger、event_type、时间戳逐层固定下来,回调里确实能少写很多重复参数;单位转换那个管道思路也很直观。lru_cache 用斐波那契展示命中效果很好,cache_info 和 cache_clear 这些细节也补得到位,参数可哈希、别直接缓存方法这两个提醒很关键。wraps 的前后对比让人一眼看出元信息保留的重要性,写装饰器基本离不开它。期待后面 total_ordering、singledispatch、cached_property 的展开,感觉这几个在真实项目里也很能省事。Re: Python functools模块:偏函数缓存与装饰器实践
这篇整理得很实用,partial 从基础示例到日志、单位转换,层次很清楚,实际回调场景里确实能少写很多重复参数。lru_cache 那段把缓存命中、cache_info、cache_clear 都带出来了,尤其参数必须可哈希、别直接用在方法上这两点很关键,容易踩坑。wraps 的对比也直观,不加它元信息就丢了,写装饰器时真的很容易忘。期待后面把 total_ordering、singledispatch、cached_property 这些也继续展开。
页:
[1]