第 32 课 · 进阶 · 19 分钟

Python functools

用缓存、部分应用和折叠复用函数行为。

functoolslru_cachepartialreduce

查价函数一天跑几千次,价格却从不变。加个缓存只要一行。缓存什么时候会悄悄变成 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 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← collections枚举 →