别让缓存变成 Bug:深入理解 functools.lru_cache 的限制、陷阱与最佳实践
在 Python 开发中,functools.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)
一个原本会指数级重复计算的递归函数,瞬间变得飞快。初学者第一次看到它,往往会觉得这像魔法;资深开发者也经常在配置读取、解析、递归搜索、网络请求封装、动态规划等场景中使用它。
但缓存从来不是免费的午餐。
lru_cache 的确能提升性能,但它也可能带来内存泄漏、脏数据、并发重复计算、参数不可哈希、对象生命周期异常、异步函数缓存错误等问题。很多线上问题不是因为没有缓存,而是因为“缓存得太随意”。
这篇文章不只讲 lru_cache 怎么用,更重点讨论:它有哪些限制?哪些场景不能用?使用时最容易踩哪些坑?以及如何写出既快又稳的缓存代码。
一、lru_cache 到底做了什么?
lru_cache 全称可以理解为 Least Recently Used Cache,也就是“最近最少使用缓存”。
它的基本思想很朴素:
当一个函数被调用时,如果之前用相同参数调用过,就直接返回上一次的结果;如果没调用过,就执行函数并把结果存起来。当缓存数量超过 maxsize 时,最久没有被使用的缓存项会被淘汰。
例如:
from functools import lru_cache
import time
@lru_cache(maxsize=3)
def slow_square(n):
print(f"正在计算 {n} 的平方")
time.sleep(1)
return n * n
print(slow_square(2)) # 计算
print(slow_square(2)) # 直接命中缓存
print(slow_square(3)) # 计算
print(slow_square(4)) # 计算
print(slow_square(5)) # 计算,缓存可能淘汰最旧项
你会发现第二次调用 slow_square(2) 几乎瞬间返回,因为结果已经缓存了。
这就是 lru_cache 最适合的场景:同样的输入会频繁出现,而且函数计算成本较高,结果又稳定不变。
典型场景包括:
# 递归动态规划
@lru_cache(maxsize=None)
def climb_stairs(n):
if n <= 2:
return n
return climb_stairs(n - 1) + climb_stairs(n - 2)
# 配置解析
@lru_cache(maxsize=32)
def parse_config(path):
with open(path, encoding="utf-8") as f:
return f.read()
# 文本规则编译
import re
from functools import lru_cache
@lru_cache(maxsize=128)
def compile_pattern(pattern):
return re.compile(pattern)
但请注意:上面这些例子成立的前提是,函数结果相对稳定。如果输入一样但结果可能变化,缓存就可能变成问题。
二、第一个限制:参数必须是可哈希对象
lru_cache 的缓存底层依赖类似字典的结构,因此函数参数必须可以作为缓存键。换句话说,参数必须是可哈希的。
下面这段代码会报错:
from functools import lru_cache
@lru_cache(maxsize=128)
def total(numbers):
return sum(numbers)
print(total([1, 2, 3]))
错误通常类似:
TypeError: unhashable type: 'list'
因为 list 是可变对象,不可哈希。
解决方法是把可变对象转换为不可变对象:
from functools import lru_cache
@lru_cache(maxsize=128)
def total(numbers):
return sum(numbers)
print(total(tuple([1, 2, 3])))
如果参数是字典,可以转换成排序后的元组:
from functools import lru_cache
def freeze_dict(d):
return tuple(sorted(d.items()))
@lru_cache(maxsize=128)
def build_query(params):
return "&".join(f"{k}={v}" for k, v in params)
params = {"page": 1, "size": 20}
print(build_query(freeze_dict(params)))
但这里还有一个隐藏问题:如果字典中嵌套了列表、字典、集合,仅仅 tuple(sorted(d.items())) 仍然不够。你可能需要一个递归冻结函数:
def freeze(obj):
if isinstance(obj, dict):
return tuple(sorted((k, freeze(v)) for k, v in obj.items()))
if isinstance(obj, list):
return tuple(freeze(x) for x in obj)
if isinstance(obj, set):
return frozenset(freeze(x) for x in obj)
return obj
实践建议是:不要让复杂可变对象直接进入被缓存函数。 更好的设计是先把输入规范化,再调用缓存函数。
def query_api(params):
frozen = freeze(params)
return _query_api_cached(frozen)
@lru_cache(maxsize=256)
def _query_api_cached(frozen_params):
params = dict(frozen_params)
# 执行真正查询
return params
这样代码边界更清晰,出错时也更容易排查。
三、第二个陷阱:缓存的是返回值引用,不是“快照”
很多人以为缓存只是“记住结果”,但忽略了一个事实:如果返回值是可变对象,缓存保存的是这个对象的引用。
看一个例子:
from functools import lru_cache
@lru_cache(maxsize=128)
def get_items():
return []
items1 = get_items()
items1.append("bug")
items2 = get_items()
print(items2)
输出是:
['bug']
这意味着第一次拿到的列表被修改后,缓存中的对象也被污染了。
这类问题非常隐蔽。在项目中,你可能缓存了一个列表、字典、DataFrame 或自定义对象,然后某个调用方无意中修改了它,后续所有命中缓存的调用都会拿到被修改后的对象。
解决方法有三种。
第一,返回不可变对象:
@lru_cache(maxsize=128)
def get_roles():
return ("admin", "editor", "viewer")
第二,在外层返回副本:
from functools import lru_cache
import copy
@lru_cache(maxsize=128)
def _load_config():
return {"debug": False, "timeout": 3}
def load_config():
return copy.deepcopy(_load_config())
第三,明确约定返回对象不可修改。但这种方式依赖团队纪律,不如代码层面安全。
我的建议是:缓存函数优先返回字符串、数字、元组、冻结数据结构,慎重返回列表和字典。
四、第三个陷阱:缓存可能让数据过期
lru_cache 只关心函数参数,不知道外部世界发生了什么变化。
例如:
from functools import lru_cache
@lru_cache(maxsize=128)
def read_user_name(user_id):
# 假设这里查询数据库
return query_database(user_id)
如果数据库中的用户名变了,但 user_id 没变,那么 read_user_name(user_id) 仍然会返回旧值。
这就是缓存中最常见的问题:脏数据。
对纯计算函数来说,缓存通常很安全:
@lru_cache(maxsize=None)
def factorial(n):
return 1 if n == 0 else n * factorial(n - 1)
但对依赖外部状态的函数来说,缓存必须谨慎:
# 文件内容可能变化
@lru_cache(maxsize=32)
def read_file(path):
with open(path) as f:
return f.read()
# 数据库内容可能变化
@lru_cache(maxsize=256)
def get_user(user_id):
return db.find_user(user_id)
# 接口返回可能变化
@lru_cache(maxsize=256)
def fetch_price(symbol):
return request_price(symbol)
解决方案之一是手动清理缓存:
read_file.cache_clear()
也可以把版本号、更新时间、日期等放入参数中:
@lru_cache(maxsize=256)
def get_user(user_id, version):
return db.find_user(user_id)
user = get_user(1001, version="2026-06-21")
如果你需要基于时间过期的缓存,lru_cache 本身并不直接支持 TTL。可以自己封装:
from functools import lru_cache
import time
def ttl_cache(seconds, maxsize=128):
def decorator(func):
@lru_cache(maxsize=maxsize)
def wrapper(ttl_key, *args, **kwargs):
return func(*args, **kwargs)
def inner(*args, **kwargs):
ttl_key = int(time.time() // seconds)
return wrapper(ttl_key, *args, **kwargs)
inner.cache_clear = wrapper.cache_clear
inner.cache_info = wrapper.cache_info
return inner
return decorator
@ttl_cache(seconds=60, maxsize=256)
def get_exchange_rate(base, target):
print("调用外部接口")
return 7.2
print(get_exchange_rate("USD", "CNY"))
这里通过额外加入 ttl_key,让缓存每隔一段时间自然失效。
不过要注意,这只是简单方案。生产环境中,如果你需要分布式缓存、精确过期、主动删除某个 key、缓存预热、缓存穿透保护,应该考虑 Redis、Memcached 或专门的缓存库。
五、第四个陷阱:maxsize=None 可能导致内存无限增长
很多动态规划示例会这样写:
@lru_cache(maxsize=None)
def fib(n):
...
这在短脚本或算法题里没问题,但在 Web 服务、后台任务、常驻进程中就很危险。
maxsize=None 意味着缓存不再淘汰旧值。只要不同参数不断进入,缓存就会越来越大。
例如:
from functools import lru_cache
@lru_cache(maxsize=None)
def normalize_text(text):
return text.strip().lower()
for i in range(10_000_000):
normalize_text(f"user_input_{i}")
如果每个输入都不同,这个缓存几乎没有命中率,却持续占用内存。
这类缓存是最糟糕的:没有带来速度收益,却带来了内存压力。
因此,除非你非常确定参数空间有限,否则不要轻易使用 maxsize=None。
更稳妥的做法:
@lru_cache(maxsize=1024)
def normalize_text(text):
return text.strip().lower()
同时定期观察缓存命中率:
print(normalize_text.cache_info())
输出类似:
CacheInfo(hits=3000, misses=500, maxsize=1024, currsize=500)
你需要重点关注:
hits:命中次数misses:未命中次数currsize:当前缓存数量maxsize:最大缓存容量
如果 misses 很高,hits 很低,说明这个缓存价值不大,甚至可能应该删除。
六、第五个陷阱:关键字参数顺序可能影响缓存键
很多人以为下面两种调用完全一样:
f(a=1, b=2)
f(b=2, a=1)
从函数语义看,它们确实一样。但对缓存来说,不同的参数模式可能被视为不同调用。
示例:
from functools import lru_cache
@lru_cache(maxsize=128)
def combine(a, b):
print("真正执行")
return a + b
print(combine(a=1, b=2))
print(combine(b=2, a=1))
print(combine.cache_info())
你可能会看到两次未命中。
解决方法是统一调用风格。比如团队约定:被缓存函数尽量使用位置参数,或者在外层做参数规范化。
def combine(a, b):
return _combine_cached(a, b)
@lru_cache(maxsize=128)
def _combine_cached(a, b):
print("真正执行")
return a + b
如果你的函数参数很多,建议用 dataclass 或明确的 key 对象来规范缓存键:
from dataclasses import dataclass
from functools import lru_cache
@dataclass(frozen=True)
class QueryKey:
page: int
size: int
keyword: str
@lru_cache(maxsize=256)
def search_cached(key: QueryKey):
return f"search: {key.keyword}, page={key.page}, size={key.size}"
key = QueryKey(page=1, size=20, keyword="python")
print(search_cached(key))
frozen=True 让 dataclass 实例不可变且可哈希,更适合作为缓存键。
七、第六个陷阱:typed=True 不是深度类型区分
lru_cache 有一个参数 typed:
@lru_cache(maxsize=128, typed=True)
def func(x):
return x
设置 typed=True 后,某些不同类型的参数会被分开缓存。例如 1 和 1.0 可能被视为不同缓存项。
但这并不意味着它会递归检查容器内部的所有类型。
例如:
from functools import lru_cache
from decimal import Decimal
from fractions import Fraction
@lru_cache(maxsize=128, typed=True)
def show(value):
print("真正执行")
return value
show(("answer", Decimal(42)))
show(("answer", Fraction(42)))
print(show.cache_info())
你不能简单地以为 typed=True 会对所有嵌套内容做深度类型区分。它主要作用于函数的直接参数层面。
实践建议是:如果类型差异会影响结果,就不要完全依赖 typed=True,而应该主动构造明确的缓存键。
def make_key(value):
return (type(value).__name__, repr(value))
@lru_cache(maxsize=128)
def process_cached(key):
type_name, raw = key
return type_name, raw
缓存键应该服务于业务语义,而不只是服务于 Python 的默认规则。
八、第七个陷阱:不要缓存有副作用的函数
缓存最适合纯函数。
所谓纯函数,就是同样输入一定得到同样输出,并且不会修改外部状态。
例如:
def add(a, b):
return a + b
但下面这些函数都不适合直接缓存:
import time
import random
@lru_cache(maxsize=128)
def now():
return time.time()
@lru_cache(maxsize=128)
def roll():
return random.randint(1, 6)
第一次调用后,后续调用会一直返回缓存结果。
这显然违背了函数语义。
更复杂的副作用包括:
@lru_cache(maxsize=128)
def send_email(user_id):
email_service.send(user_id)
return True
这个函数如果被缓存,第二次调用可能根本不会发送邮件。
还有:
@lru_cache(maxsize=128)
def create_order(user_id, product_id):
return order_service.create(user_id, product_id)
如果订单创建被缓存,那后果就更严重。
一句话:读操作可以考虑缓存,写操作不要用 lru_cache。
更准确地说,适合缓存的是:
- 纯计算
- 幂等读取
- 稳定映射
- 昂贵但结果可复用的函数
不适合缓存的是:
- 发送消息
- 写数据库
- 创建订单
- 获取当前时间
- 生成随机数
- 依赖频繁变化的外部状态
九、第八个陷阱:缓存实例方法时,self 也会进入缓存
这是高级开发者也容易踩的坑。
看代码:
from functools import lru_cache
class UserService:
def __init__(self, name):
self.name = name
@lru_cache(maxsize=128)
def get_profile(self, user_id):
print(f"{self.name} 查询用户 {user_id}")
return {"user_id": user_id}
这里 self 是方法的第一个参数,因此它也会成为缓存键的一部分。
这会带来两个问题。
第一,不同实例之间不共享缓存:
a = UserService("A")
b = UserService("B")
a.get_profile(1)
b.get_profile(1)
即使 user_id 相同,也会分别缓存,因为 self 不同。
第二,缓存会持有 self 的引用,可能延长实例生命周期。如果实例很大,或者创建很多实例,就可能造成内存问题。
更推荐的做法是把缓存放到静态函数或模块级函数上:
from functools import lru_cache
@lru_cache(maxsize=1024)
def get_profile_cached(user_id):
print(f"查询用户 {user_id}")
return {"user_id": user_id}
class UserService:
def get_profile(self, user_id):
return get_profile_cached(user_id)
如果缓存确实依赖实例状态,可以把影响结果的状态显式放进 key:
from functools import lru_cache
@lru_cache(maxsize=1024)
def get_profile_cached(env, user_id):
print(f"[{env}] 查询用户 {user_id}")
return {"env": env, "user_id": user_id}
class UserService:
def __init__(self, env):
self.env = env
def get_profile(self, user_id):
return get_profile_cached(self.env, user_id)
这样缓存键更可控,也更容易测试。
十、第九个陷阱:多线程下可能重复计算
lru_cache 在多线程下会保证缓存内部数据结构不被破坏,但它不保证同一个 key 在并发请求下只执行一次。
例如两个线程几乎同时调用:
get_data("python")
如果缓存中还没有这个 key,两个线程可能都会进入真实函数执行。等第一个线程执行完写入缓存时,第二个线程可能已经开始计算了。
这在普通计算中通常没问题,但如果函数背后是昂贵 I/O,比如调用外部接口、访问数据库、读取大文件,就可能造成短时间内的重复压力。
如果你需要“同一个 key 同一时间只能计算一次”,就需要额外的锁,也就是常说的 singleflight 思路。
一个简单版本如下:
from functools import lru_cache
from threading import Lock
_lock = Lock()
@lru_cache(maxsize=128)
def _load_data_cached(key):
print("真正加载数据")
return key.upper()
def load_data(key):
with _lock:
return _load_data_cached(key)
这个版本比较粗暴,所有 key 共用一把锁,并发性能一般。更精细的做法是每个 key 一把锁,但实现会复杂一些。
实践中要记住:lru_cache 是缓存工具,不是并发控制工具。
十一、第十个陷阱:不要直接缓存 async 函数和生成器函数
这是 Python 进阶场景里非常常见的坑。
错误示例:
from functools import lru_cache
import asyncio
@lru_cache(maxsize=128)
async def fetch_data(url):
await asyncio.sleep(1)
return {"url": url}
看起来似乎没问题,但 async 函数调用后返回的是 coroutine 对象。缓存 coroutine 对象通常不是你想要的结果。一个 coroutine 被 await 之后不能再次正常复用。
生成器也类似:
@lru_cache(maxsize=128)
def numbers():
yield 1
yield 2
yield 3
缓存生成器对象会导致迭代状态被共享。第一次消费完,后续再拿到的可能就是已经耗尽的生成器。
正确做法是缓存最终结果,而不是缓存 coroutine 或 generator 对象。
例如对异步函数,可以用普通函数缓存同步构造结果,或者使用专门支持 async 的缓存库。
对生成器,可以返回元组:
from functools import lru_cache
@lru_cache(maxsize=128)
def numbers():
return tuple(range(3))
for n in numbers():
print(n)
如果数据量很大,本来就是为了流式处理,那就不应该把它整体缓存起来。
十二、第十一个陷阱:缓存命中率低时,lru_cache 反而拖慢程序
缓存不是没有成本的。
每次调用被 lru_cache 包装的函数时,Python 都需要构造缓存键、计算哈希、查找字典、维护 LRU 顺序。这些操作虽然通常很快,但不是零成本。
如果函数本身非常简单,缓存反而可能让它更慢:
from functools import lru_cache
@lru_cache(maxsize=128)
def add(a, b):
return a + b
这种函数本来就只需要一次加法,缓存没有意义。
另一个问题是输入高度离散:
@lru_cache(maxsize=1024)
def process_event(event_id):
return heavy_parse(event_id)
如果每个 event_id 只出现一次,那么缓存命中率接近 0。你付出了缓存管理成本,却几乎没有收益。
所以使用缓存前,最好问自己三个问题:
- 这个函数是否足够昂贵?
- 相同参数是否会重复出现?
- 结果是否稳定、可复用?
如果三个问题中有两个答案是否定的,就不要急着加缓存。
十三、第十二个陷阱:递归函数的 maxsize 太小会抖动
lru_cache 经常用于递归动态规划,但如果 maxsize 设置太小,可能会发生缓存抖动。
例如:
from functools import lru_cache
@lru_cache(maxsize=2)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35))
print(fib.cache_info())
理论上 Fibonacci 很适合缓存,但 maxsize=2 太小,很多中间结果刚缓存就被淘汰,导致重复计算仍然严重。
对递归动态规划来说,如果问题规模可控,可以使用:
@lru_cache(maxsize=None)
def dp(state):
...
但如果状态空间不可控,则需要估计状态数量,设置合理上限,或者改用显式表结构。
例如:
def fib_iter(n):
if n < 2:
return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
有些问题并不需要缓存,迭代写法反而更清晰、更省内存。
十四、第十三个陷阱:异常不会被缓存
如果被缓存函数抛出异常,lru_cache 不会把异常结果保存下来。
from functools import lru_cache
@lru_cache(maxsize=128)
def unstable(x):
print("真正执行")
if x < 0:
raise ValueError("x 不能为负数")
return x
for _ in range(3):
try:
unstable(-1)
except ValueError:
pass
你会看到“真正执行”打印三次。
这通常是合理的,因为异常可能是暂时性的。但如果某些失败结果本身也很昂贵,比如查询发现某个 key 不存在,每次都访问数据库,可能会造成重复压力。
可以改成缓存显式结果:
@lru_cache(maxsize=128)
def find_user(user_id):
user = query_user(user_id)
if user is None:
return None
return user
而不是用异常表达可预期的业务结果。
十五、第十四个陷阱:无法按单个 key 精准删除
lru_cache 提供了:
func.cache_clear()
但它清理的是整个缓存,而不是某一个 key。
例如你缓存了用户数据:
@lru_cache(maxsize=1024)
def get_user(user_id):
return query_user(user_id)
当用户 1001 更新时,你可能只想删除 get_user(1001) 的缓存。但标准 lru_cache 没有公开的单 key 删除接口。
你只能:
get_user.cache_clear()
这会导致所有用户缓存失效。
如果你有强烈的单 key 删除需求,说明 lru_cache 可能不是最合适的工具。可以考虑:
- 自己维护字典缓存
- 使用
cachetools - 使用 Redis
- 在 key 中加入版本号
- 按业务维度拆分缓存函数
例如版本号方案:
@lru_cache(maxsize=1024)
def get_user_cached(user_id, version):
return query_user(user_id)
def get_user(user_id):
version = get_user_version(user_id)
return get_user_cached(user_id, version)
当用户更新时,只要版本号变化,就会自然绕过旧缓存。
十六、如何判断一个函数是否适合 lru_cache?
可以用下面这张检查表。
适合使用:
| 判断项 | 说明 |
|---|---|
| 结果确定 | 同样输入总是同样输出 |
| 无副作用 | 不写数据库、不发消息、不修改外部状态 |
| 调用昂贵 | 计算复杂、I/O 成本高、解析成本高 |
| 参数重复 | 相同参数会多次出现 |
| 参数可哈希 | 能安全作为缓存键 |
| 返回值安全 | 不会被调用方随意修改 |
| 状态可控 | 不依赖频繁变化的外部数据 |
不适合使用:
| 场景 | 原因 |
|---|---|
| 当前时间 | 每次结果应不同 |
| 随机数 | 缓存会破坏随机性 |
| 写操作 | 命中缓存后副作用不再执行 |
| 大量唯一输入 | 命中率低,浪费内存 |
| async 函数 | 容易缓存 coroutine 对象 |
| 生成器函数 | 容易缓存迭代状态 |
| 需要 TTL | 标准 lru_cache 不直接支持 |
| 需要单 key 删除 | 标准 lru_cache 不支持 |
十七、一个更贴近实战的封装案例
假设我们要做一个 Markdown 文章渲染服务。文章内容来自数据库,渲染 Markdown 比较耗时,但文章更新不频繁。
错误写法:
@lru_cache(maxsize=1024)
def render_article(article_id):
article = db.get_article(article_id)
return markdown_to_html(article.content)
问题是:文章更新后,article_id 不变,HTML 仍然是旧的。
更好的写法:
from functools import lru_cache
@lru_cache(maxsize=1024)
def _render_article_cached(article_id, updated_at):
article = db.get_article(article_id)
return markdown_to_html(article.content)
def render_article(article_id):
article_meta = db.get_article_meta(article_id)
return _render_article_cached(article_id, article_meta.updated_at)
这里把 updated_at 加入缓存键。只要文章更新时间变化,缓存自然失效。
如果担心返回 HTML 字符串太大,可以限制缓存数量:
@lru_cache(maxsize=256)
def _render_article_cached(article_id, updated_at):
...
并定期观察:
info = _render_article_cached.cache_info()
print(info)
如果命中率低,可以降低 maxsize 或取消缓存;如果命中率高但频繁淘汰,可以适当增大。
十八、推荐的使用模板
在真实项目中,我喜欢把公开函数和缓存函数分开。
from functools import lru_cache
def get_product_price(product_id, region):
key = _make_price_key(product_id, region)
return _get_product_price_cached(key)
def _make_price_key(product_id, region):
return (str(product_id), region.upper())
@lru_cache(maxsize=512)
def _get_product_price_cached(key):
product_id, region = key
return query_price(product_id, region)
这种写法有几个优点:
第一,外部调用方不需要关心缓存键如何构造。
第二,参数规范化集中在一个地方。
第三,未来如果要替换 Redis、加 TTL、加日志、加监控,不需要大面积修改业务代码。
第四,测试更容易写。
你还可以加入缓存观测:
def print_cache_status():
print(_get_product_price_cached.cache_info())
或者在调试时绕过缓存:
raw_func = _get_product_price_cached.__wrapped__
十九、最佳实践总结
最后,把经验浓缩成几条实用规则。
第一,优先缓存纯函数。越接近数学函数,越适合 lru_cache。
第二,不要盲目使用 maxsize=None。常驻服务中尤其要谨慎。
第三,缓存前先确认参数是否可哈希,复杂对象要先规范化。
第四,返回值尽量不可变,避免缓存对象被外部污染。
第五,依赖数据库、文件、接口的数据,要设计失效策略。
第六,实例方法上使用 lru_cache 要非常谨慎,因为 self 会进入缓存。
第七,异步函数和生成器函数不要直接套 lru_cache。
第八,定期查看 cache_info(),用数据判断缓存是否真的有效。
第九,如果需要 TTL、分布式缓存、单 key 删除,不要勉强 lru_cache,该上专业缓存就上专业缓存。
第十,缓存是优化手段,不是架构补丁。不要用缓存掩盖低效查询、混乱设计或错误的数据流。
二十、结语:缓存让程序更快,也要求开发者更清醒
functools.lru_cache 是 Python 标准库里非常优雅的工具。它简单、强大、几乎零学习成本,一行装饰器就能带来显著性能提升。
但真正成熟的开发者不会只问:“能不能加缓存?”
他们会继续追问:
这个函数是否稳定?
缓存多久合适?
参数空间有多大?
命中率是多少?
内存会不会增长?
数据更新后如何失效?
并发请求下会不会重复计算?
返回对象会不会被污染?
这些问题看似琐碎,却决定了缓存到底是性能加速器,还是未来某个凌晨三点的线上事故。
Python 的魅力,正是在这种地方体现出来:它给你简单的工具,也给你足够的空间去写出优雅、可靠、有温度的工程代码。
如果你刚开始学习 Python,lru_cache 会让你第一次感受到“少写很多代码,却能快很多”的快乐。
如果你已经是资深开发者,lru_cache 也值得你反复审视。因为真正的性能优化,从来不是堆装饰器,而是理解数据、理解状态、理解边界。
愿你写下的每一层缓存,都有清晰的理由;愿你的 Python 程序,不只是跑得快,也跑得稳。
互动问题
你在项目中使用过 functools.lru_cache 吗?有没有遇到过缓存失效、内存增长、脏数据或异步缓存相关的问题?
面对越来越复杂的 Python 应用场景,你认为标准库缓存工具未来最需要补足的能力是什么:TTL、单 key 删除、异步支持,还是更强的可观测性?
欢迎在评论区分享你的经验。很多时候,一个真实踩坑案例,比十段教程都更有价值。


被折叠的 条评论
为什么被折叠?



