Python 的缓存机制主要可以分为两大类:

- 内置缓存:由 Python 解释器自动管理,主要针对小整数和字符串等不可变对象,以提高性能和内存效率。
- 手动/显式缓存:由开发者通过编程技术实现,用于缓存函数的计算结果,避免重复计算,提升程序运行速度。
下面我们分别深入探讨这两类机制。
内置缓存机制
这部分机制是 Python 解释器在底层自动完成的,开发者通常无需关心,但了解它有助于我们更好地理解 Python 的行为和性能特性。
整数缓存
Python 会对小整数(通常在 -5 到 256 的范围内)进行缓存。
- 原理:像
-5, -4, ..., 0, 1, ..., 256这些整数在程序中非常常用,为了避免在内存中为每一个相同的小整数都创建一个新对象,Python 解释器在启动时会预先创建这些整数对象,并将它们存入一个对象池中,当代码中需要使用这些整数时,直接从这个池中引用,而不是重新创建。 - 验证:我们可以使用
is操作符(它检查的是两个对象的身份,即内存地址是否相同)来验证这一点。
# a 和 b 都指向了同一个缓存中的 100 对象 a = 100 b = 100 print(a is b) # 输出: True # c 和 d 指向了不同的对象,因为 1000 超出了缓存范围 c = 1000 d = 1000 print(c is d) # 输出: False (CPython 中通常为 False,但这是实现细节,不应依赖) # 如果手动创建一个 1000 的对象,并让另一个变量指向它,它们会是同一个对象 e = 1000 f = e print(e is f) # 输出: True
- 为什么是 -5 到 256? 这个范围是一个在 CPython 实现中约定俗成的优化选择,它足够大,可以覆盖绝大多数日常使用的整数,同时又足够小,不会占用过多内存。注意: 这个范围是 CPython 的实现细节,对于其他 Python 解释器(如 Jython, IronPython)或未来版本可能会有所不同,因此不应该在你的代码逻辑中依赖这个特性。
字符串缓存
字符串的缓存机制比整数更复杂一些,主要遵循以下规则:

- 字符串字面量:对于在代码中直接定义的字符串(字面量),如果两个字符串的值相同,Python 可能会复用同一个对象,以节省内存。
- 字符串驻留:这是一种更广泛的优化策略,Python 会尝试将相同的字符串对象只保留一份,但对于非字面量字符串(如通过拼接、循环生成的),这种行为不保证会发生。
# 字面量字符串的缓存 s1 = "hello world" s2 = "hello world" print(s1 is s2) # 输出: True (在大多数解释器中为 True) # 通过拼接创建的字符串,不一定被缓存 s3 = "hello " + "world" print(s3 is s2) # 输出: 可能是 True,也可能是 False,不确定!不要依赖。 # 使用 intern() 函数强制驻留 s4 = "hello world" s5 = "hello world" s6 = "".join(["hello", " ", "world"]) # 手动进行字符串驻留 s6_interned = sys.intern(s6) print(s5 is s6) # 输出: False (很可能) print(s5 is s6_interned) # 输出: True
sys.intern():这是一个手动干预字符串缓存的方法,它会将字符串放入一个全局的字符串表中,如果表中已经存在相同的字符串,它会返回表中那个字符串的引用;否则,它会将当前字符串加入表中并返回其引用,这在处理大量重复字符串(如在解析大量文本数据时)时可以显著减少内存占用,但会增加创建字符串时的开销。
手动/显式缓存
手动缓存的核心思想是“空间换时间”,即用额外的内存空间来存储计算结果,当再次遇到相同的输入时,直接从内存中读取结果,而不是重新执行一遍耗时的计算。
使用字典或缓存类
最基础的手动缓存方式就是使用字典。
# 一个计算斐波那契数列的函数,非常耗时
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 使用字典进行手动缓存
cache = {}
def fibonacci_cached(n):
if n in cache:
return cache[n]
if n < 2:
result = n
else:
result = fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
cache[n] = result
return result
# --- 测试 ---
import time
start_time = time.time()
print(fibonacci(35)) # 计算耗时较长
end_time = time.time()
print(f"普通版本耗时: {end_time - start_time:.4f} 秒")
start_time = time.time()
print(fibonacci_cached(35)) # 首次计算后,后续调用会非常快
end_time = time.time()
print(f"缓存版本耗时: {end_time - start_time:.4f} 秒")
# 再次调用,速度极快
start_time = time.time()
print(fibonacci_cached(35))
end_time = time.time()
print(f"缓存版本(第二次调用)耗时: {end_time - start_time:.4f} 秒")
functools.lru_cache 装饰器
Python 标准库 functools 提供了一个非常强大且易于使用的装饰器 lru_cache,它为我们实现了上述缓存逻辑,并且功能更完善。
- LRU:全称是 Least Recently Used(最近最少使用),当缓存空间满时,它会淘汰掉最久没有被使用过的缓存项。
- 使用方法:只需在函数定义前加上
@lru_cache()装饰器即可。
from functools import lru_cache
import time
# 原始的、耗时的函数
@lru_cache(maxsize=None) # maxsize=None 表示缓存大小无限制
def fibonacci_lru(n):
if n < 2:
return n
return fibonacci_lru(n - 1) + fibonacci_lru(n - 2)
# --- 测试 ---
start_time = time.time()
print(fibonacci_lru(35))
end_time = time.time()
print(f"lru_cache 版本耗时: {end_time - start_time:.4f} 秒")
# 再次调用,速度极快
start_time = time.time()
print(fibonacci_lru(35))
end_time = time.time()
print(f"lru_cache 版本(第二次调用)耗时: {end_time - start_time:.4f} 秒")
# 查看缓存信息
print(f"缓存信息: {fibonacci_lru.cache_info()}")
# 输出类似: CacheInfo(hits=35, misses=36, maxsize=None, currsize=36)
# hits: 命中次数, misses: 未命中(重新计算)次数
lru_cache 的优点:

- 简单易用:只需一行代码,无需手动管理字典。
- 线程安全:它是线程安全的。
- 功能强大:
maxsize:可以设置缓存的最大条目数,防止内存无限增长。typed:如果设置为True,不同类型的参数会被视为不同的缓存键。f(3)和f(3.0)会被缓存两次。cache_info()和cache_clear():提供了查看缓存状态和清空缓存的方法。
适用场景:
- 纯函数:函数的输出仅依赖于输入参数,且没有副作用(如修改全局变量、打印日志等)。
- 计算密集型任务:如复杂的数学计算、文件解析、网络请求等。
- 重复调用:函数会被多次使用相同的参数调用。
__slots__ 与缓存
__slots__ 是一个特殊属性,它通常不被直接归类为“缓存机制”,但它通过一种非常高效的方式“缓存”了实例的属性信息,从而极大地优化了内存和性能。
- 问题:在 Python 中,每个实例默认都有一个
__dict__字典来存储其属性,这非常灵活,但也带来了内存和性能开销。 __slots__的作用:当你在一个类中定义__slots__时,Python 就不会再为该类的实例创建__dict__,相反,它会为你在__slots__中列出的属性创建固定的、预分配的“槽位”。
class WithoutSlots:
def __init__(self, a, b):
self.a = a
self.b = b
class WithSlots:
__slots__ = ['a', 'b'] # 定义实例可以拥有的属性
def __init__(self, a, b):
self.a = a
self.b = b
# --- 测试 ---
# 1. 内存占用
import sys
wos = WithoutSlots(1, 2)
ws = WithSlots(1, 2)
print(f"WithoutSlots 内存占用: {sys.getsizeof(wos)} bytes") # 包含 __dict__ 的开销
print(f"WithSlots 内存占用: {sys.getsizeof(ws)} bytes") # 更小
# 2. 属性访问速度
import timeit
def access_without_slots():
obj = WithoutSlots(1, 2)
return obj.a
def access_with_slots():
obj = WithSlots(1, 2)
return obj.a
print(f"\nWithoutSlots 访问时间: {timeit.timeit(access_without_slots, number=1000000):.4f} 秒")
print(f"WithSlots 访问时间: {timeit.timeit(access_with_slots, number=1000000):.4f} 秒")
__slots__ 的优缺点:
- 优点:
- 节省内存:没有了
__dict__,内存占用显著减少。 - 访问速度更快:属性的访问是通过一个固定的偏移量完成的,比在字典中查找键要快。
- 节省内存:没有了
- 缺点:
- 失去灵活性:无法在运行时动态添加
__slots__中未定义的属性。 - 没有
__dict__:如果你需要存储额外的、未在__slots__中声明的属性,会直接报错。 - 子类复杂性:如果子类没有定义
__slots__,它会继承父类的__slots__,但同时仍然会拥有自己的__dict__,可能达不到预期的内存节省效果。
- 失去灵活性:无法在运行时动态添加
| 缓存类型 | 核心机制 | 优点 | 缺点/注意事项 | 适用场景 |
|---|---|---|---|---|
| 内置缓存 | 解释器自动对小整数和字符串字面量进行对象复用。 | 提升性能,节省内存。 | 实现细节,不应在代码逻辑中依赖。 | Python 解释器内部优化。 |
| 手动缓存 (字典) | 用字典存储函数参数和返回值。 | 灵活,易于理解。 | 需要手动管理,代码冗余,可能有线程安全问题。 | 简单场景,学习缓存原理。 |
lru_cache |
functools 提供的装饰器,实现 LRU 策略的自动缓存。 |
极其简单,线程安全,功能丰富(maxsize, typed, cache_info)。 |
仅适用于纯函数。 | 强烈推荐用于优化纯函数的重复计算。 |
__slots__ |
通过固定属性槽位来“缓存”实例属性信息。 | 显著节省内存,提升属性访问速度。 | 失去动态添加属性的灵活性,没有 __dict__。 |
用于创建大量实例且属性固定的类,如数据模型。 |
Python 提供了从底层自动优化到开发者手动控制的多种缓存机制,在日常开发中,对于纯函数的性能优化,@lru_cache 是你的首选工具,而对于内存敏感的类设计,__slots__ 是一个强大的利器,理解这些机制,能帮助你写出更高效、更优雅的 Python 代码。
