第 21 章 内存管理与性能优化
21.1 Python 对象模型与内存布局
Python 中一切皆对象,每个对象至少包含:
import sys
x = 42
print(sys.getsizeof(x)) # 28 字节(CPython 中一个 int 对象)
# 对象的核心组成
# 1. 引用计数(8 字节)
# 2. 类型指针(8 字节)
# 3. 值(大小取决于类型)
# 各类型的基本大小
print(sys.getsizeof(0)) # 28 — int
print(sys.getsizeof(0.0)) # 24 — float
print(sys.getsizeof(True)) # 28 — bool(继承 int)
print(sys.getsizeof("")) # 49 — 空字符串
print(sys.getsizeof("a")) # 50 — 1 个 ASCII 字符
print(sys.getsizeof([])) # 56 — 空列表
print(sys.getsizeof({})) # 64 — 空字典
print(sys.getsizeof(set())) # 216 — 空集合
print(sys.getsizeof(())) # 40 — 空元组
id() 与对象身份
a = [1, 2, 3]
print(id(a)) # 内存地址(CPython 中)
print(hex(id(a))) # 十六进制地址
# 小整数缓存 (-5 ~ 256)
a = 100
b = 100
print(id(a) == id(b)) # True — 同一个对象
# 字符串驻留(interning)
a = "hello"
b = "hello"
print(a is b) # True — 短字符串被驻留
21.2 引用计数与垃圾回收
引用计数
Python 主要通过引用计数管理内存:
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(a 自身 + getrefcount 的参数)
b = a
print(sys.getrefcount(a)) # 3
c = [a, a]
print(sys.getrefcount(a)) # 5
del b
print(sys.getrefcount(a)) # 4
# 引用计数为 0 时,对象立即被销毁
class Demo:
def __del__(self):
print(f"对象被销毁")
d = Demo()
del d # 立即打印 "对象被销毁"
循环引用问题
# 引用计数无法处理循环引用
a = []
b = []
a.append(b) # a → b
b.append(a) # b → a — 循环引用!
del a, b
# 引用计数不为 0(各自被对方引用),但已无法从外部访问
# 需要垃圾回收器处理
分代垃圾回收(Generational GC)
import gc
# 三代:0(新对象)→ 1 → 2(老对象)
# 新对象在第 0 代,如果存活过一次 GC,晋升到第 1 代
print(gc.get_threshold()) # (700, 10, 10)
# 第 0 代:每 700 次分配触发
# 第 1 代:每 10 次第 0 代 GC 触发
# 第 2 代:每 10 次第 1 代 GC 触发
# 手动触发 GC
gc.collect()
# 查看 GC 统计
print(gc.get_stats())
# 查找循环引用
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print(gc.garbage) # 无法回收的对象
# 禁用 GC(高性能场景,确保无循环引用时)
gc.disable()
# ... 执行代码 ...
gc.enable()
21.3 弱引用 weakref
弱引用不增加引用计数,对象可以被正常回收:
import weakref
class HeavyObject:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"HeavyObject({self.name!r})"
obj = HeavyObject("data")
# 创建弱引用
ref = weakref.ref(obj)
print(ref()) # HeavyObject('data')
del obj
print(ref()) # None — 对象已被回收
# WeakValueDictionary — 值是弱引用的字典
cache = weakref.WeakValueDictionary()
def get_data(key):
if key in cache:
return cache[key]
data = HeavyObject(key)
cache[key] = data
return data
d = get_data("test")
print("test" in cache) # True
del d
print("test" in cache) # False — 自动清理
# finalize — 注册析构回调
obj = HeavyObject("important")
weakref.finalize(obj, print, "对象被回收了!")
del obj # 打印 "对象被回收了!"
21.4 __slots__ 与内存优化
import sys
class Regular:
def __init__(self, x, y):
self.x = x
self.y = y
class Slotted:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
r = Regular(1, 2)
s = Slotted(1, 2)
# Regular: 对象大小 + __dict__ 大小
print(sys.getsizeof(r) + sys.getsizeof(r.__dict__)) # ~200 字节
# Slotted: 没有 __dict__
print(sys.getsizeof(s)) # ~56 字节
# 大量实例时效果显著
import tracemalloc
tracemalloc.start()
regulars = [Regular(i, i) for i in range(100_000)]
print(f"Regular: {tracemalloc.get_traced_memory()[0] / 1024 / 1024:.1f} MB")
tracemalloc.stop()
tracemalloc.start()
slotteds = [Slotted(i, i) for i in range(100_000)]
print(f"Slotted: {tracemalloc.get_traced_memory()[0] / 1024 / 1024:.1f} MB")
tracemalloc.stop()
21.5 sys.getsizeof() 与 tracemalloc
sys.getsizeof — 查看单个对象大小
import sys
# 注意:不递归计算引用对象的大小
lst = [1, 2, 3]
print(sys.getsizeof(lst)) # 列表对象本身的大小,不包括元素
# 递归计算总大小
def deep_getsizeof(obj, seen=None):
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, dict):
size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen)
for k, v in obj.items())
elif isinstance(obj, (list, tuple, set, frozenset)):
size += sum(deep_getsizeof(i, seen) for i in obj)
return size
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
print(f"浅层: {sys.getsizeof(data)} 字节")
print(f"深层: {deep_getsizeof(data)} 字节")
tracemalloc — 追踪内存分配
import tracemalloc
tracemalloc.start()
# 执行代码
data = [list(range(1000)) for _ in range(1000)]
# 获取当前内存使用
current, peak = tracemalloc.get_traced_memory()
print(f"当前: {current / 1024 / 1024:.1f} MB")
print(f"峰值: {peak / 1024 / 1024:.1f} MB")
# 查看内存分配热点
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
print("\n内存分配 Top 5:")
for stat in top_stats[:5]:
print(stat)
tracemalloc.stop()
21.6 性能分析
timeit — 微基准测试
import timeit
# 比较两种方式的性能
t1 = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
t2 = timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
print(f"生成器: {t1:.4f}s")
print(f"列表推导: {t2:.4f}s")
# 比较字典查找方式
setup = "d = {i: i*2 for i in range(1000)}"
t1 = timeit.timeit("d.get(500, None)", setup=setup, number=1000000)
t2 = timeit.timeit("500 in d and d[500]", setup=setup, number=1000000)
cProfile — 函数级性能分析
import cProfile
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# 分析
cProfile.run("fibonacci(30)")
# 输出每个函数的调用次数、总时间等
# 保存结果到文件
cProfile.run("fibonacci(30)", "profile_output")
# 使用 pstats 分析结果
import pstats
p = pstats.Stats("profile_output")
p.sort_stats("cumulative")
p.print_stats(10) # 前 10 个最耗时的函数
# 也可以用装饰器形式
def profile(func):
def wrapper(*args, **kwargs):
profiler = cProfile.Profile()
profiler.enable()
result = func(*args, **kwargs)
profiler.disable()
profiler.print_stats(sort="cumulative")
return result
return wrapper
21.7 优化技巧
局部变量比全局变量快
import timeit
# 全局变量访问
x = 10
def global_access():
for _ in range(1000):
y = x # LOAD_GLOBAL
# 局部变量访问
def local_access():
x = 10
for _ in range(1000):
y = x # LOAD_FAST
t1 = timeit.timeit(global_access, number=10000)
t2 = timeit.timeit(local_access, number=10000)
print(f"全局: {t1:.4f}s, 局部: {t2:.4f}s")
# 局部变量通常快 20-30%
缓存
from functools import lru_cache, cache
# lru_cache — 带大小限制的缓存
@lru_cache(maxsize=128)
def expensive_function(n):
return sum(i * i for i in range(n))
# cache — 无大小限制(Python 3.9+)
@cache
def factorial(n):
return n * factorial(n-1) if n else 1
# 手动缓存(字典)
_cache = {}
def cached_compute(key):
if key not in _cache:
_cache[key] = heavy_computation(key)
return _cache[key]
生成器惰性求值
# 不好 — 一次性加载所有数据到内存
def process_bad(filename):
lines = open(filename).readlines() # 全部读入内存
return [process_line(line) for line in lines]
# 好 — 惰性求值,一行一行处理
def process_good(filename):
with open(filename) as f:
for line in f: # 迭代器,按需读取
yield process_line(line)
# 使用 sum/max/min 等可以直接接受生成器
total = sum(x * x for x in range(10_000_000)) # 不创建列表
选择正确的数据结构
# 成员检测:set O(1) vs list O(n)
import timeit
data_list = list(range(10000))
data_set = set(range(10000))
t1 = timeit.timeit(lambda: 9999 in data_list, number=10000)
t2 = timeit.timeit(lambda: 9999 in data_set, number=10000)
print(f"list: {t1:.4f}s, set: {t2:.4f}s")
# set 快几百倍
# 头部插入:deque O(1) vs list O(n)
from collections import deque
d = deque()
l = []
t1 = timeit.timeit(lambda: d.appendleft(0), number=100000)
t2 = timeit.timeit(lambda: l.insert(0, 0), number=100000)
print(f"deque: {t1:.4f}s, list: {t2:.4f}s")
字符串拼接
# 不好 — O(n²)
result = ""
for s in string_list:
result += s
# 好 — O(n)
result = "".join(string_list)
# 好 — 用列表收集再 join
parts = []
for item in data:
parts.append(transform(item))
result = "".join(parts)
# 更好 — 推导式 + join
result = "".join(transform(item) for item in data)
本章小结:理解 Python 的内存管理(引用计数 + 分代 GC)是写出高性能代码的基础。实用优化策略:用
__slots__减少内存、用生成器代替列表、选择正确的数据结构(set vs list)、缓存重复计算。优化前先用 cProfile/timeit 定位瓶颈,不要过早优化。