第8章 8.3 functools 函数工具
「上章回顾」:上一章我们学会了用 itertools 把复杂的循环拆成一个个小零件,像搭乐高一样组合出各种迭代器玩法。但你有没有遇到过这种情况——同一个函数传同样的参数,被重复调用了 N 次,每次都要重新算一遍,浪费了大把时间?
今天我们要学的 functools,就是来解决这个痛点的。它专门给函数提供「加速包」——缓存计算结果、固定部分参数、简化装饰器写法。学会了它,你的代码能跑得更快、更优雅。
🎯 开场 3 分钟:为什么要学这个?
场景还原:你写了一个计算斐波那契数列的函数:
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
当你调用 fib(30) 的时候,这个函数被调用了 268 万次!其中大量计算是完全重复的。
再比如:你写了一个查询天气的函数,每次用户查询都去请求远程 API,既慢又浪费钱。
痛点总结:
1. 重复计算——同样的参数,结果算了一遍又一遍
2. 重复请求——同样的查询,每次都发网络请求
3. 代码重复——写一个带额外功能的装饰器,原函数的信息全丢了
学完本文你能:
- 用 lru_cache 给函数加缓存,让重复调用秒返回
- 用 partial 固定部分参数,少写重复代码
- 用 wraps 写装饰器不丢元数据
- 用 reduce 把序列浓缩成单个值
🧱 基础 25 分钟:核心概念
8.3.1 lru_cache —— 函数的「记忆面包」
生活类比:你有没有过这种经历——数学老师问「12×13等于多少」,你刚算完等于 156,下次问「12×13」你脱口就说答案,因为记住了嘛。lru_cache 就是给函数装一个「记忆面包」,算过的结果直接存起来,下次同样输入秒出结果。
为什么要用:避免重复计算,提升性能。
怎么用:
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(30)) # 输出: 832040
print(fib.cache_info()) # 输出: CacheInfo(hits=28, misses=31, ...)
解释:@lru_cache(maxsize=128) 装饰器给函数加了一层缓存,maxsize 是缓存多少个不同的参数。调用 cache_info() 可以看到缓存命中情况——hits=28 说明有 28 次直接用了缓存结果,不用重新算。

8.3.2 cache —— 无限缓存的 lru_cache
生活类比:lru_cache 像一个有容量限制的冰箱,塞满了就得扔掉旧食材。而 cache 就是无限大的仓库,随便你存多少。
适用场景:缓存的数据量可控、不会爆炸的情况(Python 3.9+)。
from functools import cache
@cache
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
print(factorial(100)) # 输出: 933262154439441526816992388562667004907159682643816214685929...
print(factorial(50)) # 输出: 30414093201713378043612608166064768844377641568960512000000000000
print(factorial(100)) # 这次秒出,因为用了缓存
注意:cache 默认 maxsize=None(无限),用的时候要确保你的函数参数不会太多,否则内存可能爆炸。
8.3.3 partial —— 预设参数的「快捷方式」
生活类比:你去奶茶店,每次都点「三分糖、加椰果」,久了店员就说「给你记上了,以后直接说『老规矩』就行」。partial 就是这个「老规矩」——固定一个函数的某些参数,生成一个新的简化版函数。
为什么要用:减少重复传参,代码更简洁。
怎么用:
from functools import partial
# 原始函数:两个参数
def power(base, exponent):
return base ** exponent
# 固定 exponent=2,生成新函数
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 输出: 25
print(cube(2)) # 输出: 8
print(power(5, 2)) # 原函数也能用,输出: 25
解释:square = partial(power, exponent=2) 创建了一个新函数,调用时只需传 base 参数,exponent 自动用 2。
8.3.4 reduce —— 把列表「压缩」成单个值
生活类比:你去超市买了 5 样东西,结账的时候收银机一件件累加,最后给你一个总价。reduce 就是这个「累加」的过程——把一个序列按某种规则「压缩」成一个值。
为什么要用:做聚合运算(如求和、求积、找最值)。
from functools import reduce
# 求乘积:1×2×3×4×5
result = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])
print(result) # 输出: 120
# 求最大值
result = reduce(lambda a, b: a if a > b else b, [3, 7, 2, 9, 4])
print(result) # 输出: 9
# 字符串拼接
words = ['Hello', ' ', 'World', '!']
result = reduce(lambda a, b: a + b, words)
print(result) # 输出: Hello World!
解释:reduce(函数, 序列) 先把序列前两个元素传给函数,结果再跟第三个元素传进去,以此类推。lambda x, y: x * y 就是「把 x 和 y 相乘」。

8.3.5 wraps —— 装饰器的「身份证保护罩」
生活类比:你给朋友送礼物,让快递员包装了一层。朋友收到后只看到包装纸,不知道里面是啥。装饰器也是「包装」——包完之后原函数的信息(名字、文档)全变成了包装纸的。wraps 就是把原函数的信息「还原」出来。
为什么要用:写装饰器时不丢原函数的 __name__、__doc__ 等元数据。
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("调用前")
result = func(*args, **kwargs)
print("调用后")
return result
return wrapper
@my_decorator
def say_hello(name):
"""这个函数用于打招呼"""
print(f"Hello, {name}!")
print(say_hello.__name__) # 输出: say_hello(而不是wrapper)
print(say_hello.__doc__) # 输出: 这个函数用于打招呼
解释:@wraps(func) 把原函数的名字和文档复制给了 wrapper,这样 say_hello.__name__ 仍然是 say_hello,而不是被装饰后变成 wrapper。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):斐波那契数列计算器——从 268 万次到 31 次
目标:用 lru_cache 优化斐波那契计算,对比优化前后性能。
代码:
import time
from functools import lru_cache
# 不用缓存的版本
def fib_no_cache(n):
if n < 2:
return n
return fib_no_cache(n-1) + fib_no_cache(n-2)
# 用缓存的版本
@lru_cache(maxsize=100)
def fib_with_cache(n):
if n < 2:
return n
return fib_with_cache(n-1) + fib_with_cache(n-2)
# 测试
n = 30
start = time.time()
result1 = fib_no_cache(n)
time1 = time.time() - start
start = time.time()
result2 = fib_with_cache(n)
time2 = time.time() - start
print(f"不用缓存: 结果={result1}, 耗时={time1:.4f}秒")
print(f"用缓存: 结果={result2}, 耗时={time2:.4f}秒")
print(f"缓存信息: {fib_with_cache.cache_info()}")
预期输出:
不用缓存: 结果=832040, 耗时=0.1823秒
用缓存: 结果=832040, 耗时=0.0000秒
缓存信息: CacheInfo(hits=28, misses=31, ...)
解释:fib_with_cache 只调用了 31 次(真正的计算次数),却得到了同样的结果,因为 28 次直接用了缓存。不用缓存的版本调用了 268 万次!
项目 2(15 分钟):数据统计工具——用 reduce 分析学生成绩
目标:从 CSV 读取学生成绩数据,用 reduce 和 partial 做统计分析。
数据文件 students.csv:
name,score,grade
张三,85,B
李四,92,A
王五,78,C
赵六,95,A
钱七,88,B
代码:
import csv
from functools import reduce, partial
# 读取CSV文件
def read_csv(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
return list(reader)
# 用reduce统计平均分
def calc_average(scores):
return reduce(lambda a, b: a + b, scores) / len(scores)
# 用reduce找最高分学生
def find_top_student(students):
return reduce(
lambda best, current: current if int(current['score']) > int(best['score']) else best,
students
)
# 用partial固定计算逻辑
calc_sum = partial(reduce, lambda a, b: a + b)
calc_count = partial(reduce, lambda a, b: a + 1)
# 主程序
students = read_csv('students.csv')
scores = [int(s['score']) for s in students]
average = calc_average(scores)
top_student = find_top_student(students)
total = calc_sum(scores)
count = calc_count(scores, 0)
print(f"学生总数: {count}")
print(f"总分: {total}")
print(f"平均分: {average:.2f}")
print(f"最高分学生: {top_student['name']} ({top_student['score']}分)")
预期输出:
学生总数: 5
总分: 438
平均分: 87.60
最高分学生: 赵六 (95分)
解释:用 reduce 做聚合统计很强大——求和、计数、找最值都可以用它。partial 把常用的 reduce 模式封装成新函数,代码更简洁。
项目 3(15 分钟):爬虫 URL 去重工具——综合运用缓存和函数工具
目标:模拟一个 URL 爬取工具,用 cache 缓存请求结果,用 partial 固定通用逻辑。
from functools import cache, partial
import time
# 模拟的API请求(实际项目中换成真实的requests.get)
call_count = 0
@cache
def fetch_page(url):
global call_count
call_count += 1
time.sleep(0.1) # 模拟网络延迟
return f"页面内容({url})"
# 用partial固定基础URL
fetch_from_site = partial(fetch_page, "https://example.com/")
# 模拟爬虫:同一URL不会重复请求
urls_to_fetch = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page1", # 重复
"https://example.com/page3",
"https://example.com/page2", # 重复
]
print("开始爬取...")
start = time.time()
results = []
for url in urls_to_fetch:
content = fetch_page(url)
results.append(content)
elapsed = time.time() - start
print(f"\n爬取完成!")
print(f"总耗时: {elapsed:.2f}秒(理论无缓存需要: {len(urls_to_fetch)*0.1:.2f}秒)")
print(f"实际请求次数: {call_count}(去重后只有{len(set(urls_to_fetch))}个独立URL)")
print(f"缓存命中: 请求相同URL时直接返回缓存结果")
预期输出:
开始爬取...
爬取完成!
总耗时: 0.30秒(理论无缓存需要: 0.50秒)
实际请求次数: 3(去重后只有3个独立URL)
缓存命中: 请求相同URL时直接返回缓存结果
解释:用了 @cache 后,重复的 URL 不会真的发请求,直接从缓存拿结果,3 次真实请求节省了 0.2 秒。这个工具稍加改造就能接真实爬虫。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:缓存未命中——参数要可哈希
# ❌ 错误示例:列表不可哈希,不能当缓存key
from functools import lru_cache
@lru_cache
def process_data(data):
return sum(data)
process_data([1, 2, 3]) # TypeError: unhashable type: 'list'
# ✅ 正确示例:用元组代替列表
@lru_cache
def process_data(data):
return sum(data)
process_data((1, 2, 3)) # 正常
原因:lru_cache 用参数做字典的 key,列表可变所以不能哈希,元组可以。
坑 2:缓存不清——修改了原函数但还是用的缓存
# ❌ 错误示例:改了函数但忘记清缓存
@lru_cache
def get_config(key):
return f"config_{key}"
print(get_config("db")) # 输出: config_db
# 假设你修改了配置逻辑...
get_config.__wrapped__ = None # 试图清除缓存?没用!
print(get_config("db")) # 还是返回 config_db,因为缓存还在
# ✅ 正确示例:调用 cache_clear() 清除缓存
@lru_cache
def get_config(key):
return f"config_{key}"
print(get_config("db")) # 输出: config_db
get_config.cache_clear() # 清除所有缓存
print(get_config("db")) # 重新计算
坑 3:maxsize 太大——内存爆炸
# ❌ 错误示例:maxsize 设太大
@lru_cache(maxsize=100000)
def heavy_func(n):
return n * 2
# ✅ 正确示例:预估合理的缓存大小
# 大多数场景 128-512 就够用了
@lru_cache(maxsize=256)
def reasonable_func(n):
return n * 2
坑 4:partial 的参数覆盖——小心陷阱
# ❌ 错误示例:关键字参数被覆盖
from functools import partial
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# 试图固定 greeting="Hi"
say_hi = partial(greet, greeting="Hi")
print(say_hi("张三")) # 输出: Hi, 张三! 没问题
print(say_hi("李四", greeting="Hello")) # 警告!greeting 被覆盖了
# ✅ 正确示例:不要在 partial 和调用时重复传参
say_hi = partial(greet, greeting="Hi")
print(say_hi("李四")) # 输出: Hi, 李四! 用 partial 设定的值
坑 5:wraps 忘了传参数——白写了
# ❌ 错误示例:wraps 没传被装饰函数
from functools import wraps
def my_decorator(func):
@wraps # ❌ 忘了传 func 参数!
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# ✅ 正确示例:wraps(func)
def my_decorator(func):
@wraps(func) # ✅ 传入原函数
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
性能小贴士:缓存大小设置有讲究
lru_cache 的 maxsize 不是越大越好:
- 命中率低:设太大浪费内存
- LRU 淘汰:缓存满了会淘汰最久未用的
经验公式:设成你预计同时使用的不同参数数量的 1.5-2 倍。如果不确定,用默认的 maxsize=128。
调试技巧:用 cache_info() 观察缓存效果
from functools import lru_cache
@lru_cache(maxsize=10)
def expensive_computation(n):
return n ** 2
# 触发缓存
expensive_computation(5)
expensive_computation(5) # 命中缓存
expensive_computation(6)
expensive_computation(7)
expensive_computation(5) # 再次命中
print(expensive_computation.cache_info())
# 输出: CacheInfo(hits=2, misses=3, ...)
解读:hits=2 表示 2 次命中缓存,misses=3 表示 3 次未命中。开发时用这个观察你的缓存策略是否有效。
✏️ 练习题
练习 1(2 分钟):用 cache 加速阶乘计算
- 输入:
factorial(10)和factorial(5) - 预期输出:
3628800和120 - 提示:直接用
@cache装饰阶乘函数
练习 2(2 分钟):用 partial 固定 power 函数的指数
- 输入:创建一个
cube = partial(power, exponent=3),然后调用cube(2) - 预期输出:
8 - 提示:先定义
power(base, exponent)函数
练习 3(3 分钟):在项目 1 基础上加判断
- 题目:给
fib_with_cache加一个判断,如果 n 是负数返回 0 - 预期输出:
fib_with_cache(-5)返回0,fib_with_cache(10)返回55 - 提示:在函数开头加
if n < 0: return 0
练习 4(5 分钟):用 reduce 处理新数据
- 输入:
[15, 20, 25, 30, 35] - 预期输出:最大值
35,最小值15,平均值25.0 - 提示:用三个不同的 reduce 逻辑
练习 5(5 分钟):分析报错并修复
- 题目:下面代码报错
TypeError: unhashable type: 'list',找出问题并修复
from functools import lru_cache
@lru_cache
def find_max(numbers):
return max(numbers)
print(find_max([3, 1, 4, 1, 5]))
- 预期输出:
5 - 提示:列表不能做缓存的 key,改成元组
作业:做一个「网站配置加载器」
需求描述:写一个配置加载器,模拟从不同网站加载配置,用缓存避免重复请求。
功能点:
1. 用 @cache 装饰加载函数,模拟 fetch_config(site)(用 time.sleep 模拟延迟)
2. 用 partial 创建一个默认站点的配置加载器
3. 用 reduce 统计多个配置加载的总耗时
加分项:
1. 用 cache_info() 展示缓存命中情况
2. 支持清除缓存重新加载
验收标准:
- 能跑起来
- 加载相同配置秒回(用了缓存)
- 加载新配置要等待(模拟请求)
- 代码有注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
1. lru_cache / cache 给函数加缓存,避免重复计算
2. partial 固定部分参数,生成简化版函数
3. wraps 写装饰器时保留原函数元数据
延伸学习:
- 官方文档:functools — Higher-order functions and operations on callable objects
- 视频:Corey Schafer 的 functools 教程
- 书籍:《Python 进阶》第三章「装饰器」
互动钩子:你在项目里有没有遇到过「同一个函数被调用 N 次但没加缓存」的情况?结果卡成什么样了?评论区聊聊,老粉优先回复!
📌 下章预告:学会了给函数加缓存、减负担,下一章我们要学习「路径操作」和「类型提示」——
pathlib让文件和文件夹操作像说话一样简单,typing让代码自己会「说话」。敬请期待「第8章 8.4 pathlib 与 typing」!

评论(0)