查价函数一天跑几千次,价格却从不变。加个缓存只要一行。缓存什么时候会悄悄变成 bug?
functools 收集了操作函数的工具。lru_cache 记住结果,partial 提前固定部分参数,reduce 把序列折叠成一个值。它们都很小,却能减少重复的包装代码。 你还会观察缓存命中、绑定关键字参数,并按类型分派。
缓存重复调用
lru_cache 会按参数保存结果。参数相同的第二次调用可以直接返回缓存值,不再执行函数体。缓存需要无上限增长时,可以使用 maxsize=None。
from functools import lru_cache
@lru_cache(maxsize=None)
def square(n):
print("calculating", n)
return n * n
print(square(3))
print(square(3))计算一次,复用两次
用 partial 固定参数
partial 会创建一个新的可调用对象,其中一些参数已经填好。剩下的参数稍后提供。它适合把通用函数改造成特定用途,例如从 power 构造 square。
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
print(square(5))专门化的函数
用 reduce 折叠
reduce 从左到右累积应用一个二元函数。可选的初始值用来播种累加器。操作天然是折叠时可以使用它;逻辑复杂时,清楚的循环往往更好。
from functools import reduce
print(reduce(lambda total, n: total + n, [1, 2, 3], 0))多个值,一个结果
只缓存纯函数和可哈希参数
lru_cache 用参数当键,所以每个参数都必须可哈希。它还假设函数是纯的:相同输入应得到相同输出。cache_info() 可以看到命中和未命中;测试里状态改变时,可以用 cache_clear() 清掉旧结果。
from functools import lru_cache
calls = []
@lru_cache(maxsize=None)
def delivery_fee(city, weight):
calls.append(city)
return 4 if weight <= 2 else 7
print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Tainan", 1.5))
print(len(calls), delivery_fee.cache_info().hits)命中时不再执行函数体
用 partial 固定关键字参数
partial 可以预填位置参数或关键字参数,返回一个新的可调用对象。它适合总是需要同一前缀或同一格式化器的回调。原函数不受影响,partial 还会保留绑定参数的引用,便于检查。
from functools import partial
def render(template, name, *, prefix=""):
return f"{prefix}{template.format(name=name)}"
greet = partial(render, "Hello, {name}!", prefix="> ")
print(greet("Maya"))
print(greet.func.__name__, greet.args, greet.keywords)带固定上下文的新函数
用 singledispatch 按类型分派
singledispatch 根据第一个参数的类型选择实现。通用函数是兜底,register() 添加更具体的处理器。它能把类型分支从长长的 if/elif 里拿出来,但不能代替对不可信输入的校验。
from functools import singledispatch
@singledispatch
def label(value):
return "other"
@label.register
def _(value: int):
return f"int:{value}"
@label.register
def _(value: str):
return f"str:{value}"
print(label(3))
print(label("tea"))
print(label(3.5))一个名字,多个类型处理器
本课要点
- lru_cache 按函数参数缓存结果。
- 结果依赖变化的外部状态时,必须清除缓存。
- partial 固定参数并返回新的可调用对象。
- reduce 把可迭代对象折叠成一个值。
- 逻辑复杂时,短循环可能比 reduce 更清楚。
- 缓存纯函数和可哈希调用,partial 固定上下文,singledispatch 替代类型分支。
常见错误
- 缓存结果依赖可变全局状态的函数。
- 以为 partial 会立刻调用函数。
- reduce 的累加器类型重要时,忘记提供初始值。
- 缓存读取可变全局状态的函数,却以为结果会自动失效。
用 lru_cache 缓存 square 函数,再用 partial(power, exponent=2) 构造一个 square。
动手试试
改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。