配套专栏:Python 全栈修炼之路 第 19 篇《内存管理与垃圾回收——从 C 源码到实战调优》
难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐
核心覆盖:对象内存开销、引用计数、循环引用、GC 调优、弱引用、内存泄漏诊断、生产级优化
前言
第十九篇从 CPython 源码级别的 PyObject 结构出发,逐层剖析了引用计数、分代回收、弱引用机制和内存诊断工具。本练习精选 6 道编程题,从基础对象内存测量到生产级内存泄漏诊断,层层递进,帮你将理论知识转化为实战能力。
题目一:对象内存开销测量器 ⭐
📌 题目描述
实现一个对象内存开销测量工具,能够递归计算任意 Python 对象的\”真实\”内存占用(包括对象本身和引用的所有子对象):
# 基础类型
measure(42) # 28 bytes
measure(\”hello\”) # 54 bytes
measure([1, 2, 3]) # 列表本身 + 3个int对象
# 嵌套结构
measure({
\”a\”: [1, 2], \”b\”: {
\”c\”: 3}}) # 递归计算所有对象
# 自定义类
class Point:
__slots__ = [\”x\”, \”y\”]
def __init__(self, x, y):
self.x = x; self.y = y
measure(Point(1, 2)) # 对比 __slots__ 和 __dict__ 的差异
💡 编程思路
这道题考察 对象内存布局的理解 + 递归遍历:
🖥️ 参考代码
import sys
from typing import Any
def measure(obj: Any, seen: set = None, depth: int = 0, max_depth: int = 10) –> int:
\”\”\”递归测量对象及其所有子对象的真实内存占用。
Args:
obj: 要测量的对象
seen: 已测量对象的 id 集合(防止循环引用和重复计算)
depth: 当前递归深度
max_depth: 最大递归深度
Returns:
总内存占用(字节)
\”\”\”
if seen is None:
seen = set()
# 防止无限递归
if depth > max_depth:
return 0
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
# 对象本身的大小
try:
total = sys.getsizeof(obj)
except TypeError:
return 0
# 递归处理容器类型
obj_type = type(obj)
if obj_type in (list, tuple, set, frozenset):
for item in obj:
total += measure(item, seen, depth + 1, max_depth)
elif obj_type is dict:
for key, value in obj.items():
total += measure(key, seen, depth + 1, max_depth)
total += measure(value, seen, depth + 1, max_depth)
elif hasattr(obj, \”__dict__\”) and obj_type.__name__ != \”type\”:
# 有 __dict__ 的自定义实例
total += measure(obj.__dict__, seen, depth + 1, max_depth)
elif hasattr(obj, \”__slots__\”):
# __slots__ 对象
for attr in obj.__slots__:
if hasattr(obj, attr):
total += measure(getattr(obj, attr), seen, depth + 1, max_depth)
return total
def compare_memory():
\”\”\”对比不同数据结构的内存占用。\”\”\”
print(\”=\” * 60)
print(\”=== 对象内存开销对比 ===\”)
print(\”=\” * 60)
# 基础类型
print(\”\\n— 基础类型 —\”)
for obj in [42, 2**30, \”\”, \”hello\”, b\”hello\”, None, True]:
print(f\” {
repr(obj):15s}: {
measure(obj):6d} bytes\”)
# 容器类型
print(\”\\n— 容器类型 —\”)
print(f\” 空列表 []: {
measure([]):6d} bytes\”)
print(f\” [1, 2, 3]: {
measure([1, 2, 3]):6d} bytes\”)
print(f\” 空字典 {
{}}: {measure({}):6d} bytes\”)
print(f\” {
{\’a\’: 1}}: {
measure({
\’a\’: 1}):6d} bytes\”)
print(f\” 空元组 (): {
measure(()):6d} bytes\”)
print(f\” (1, 2, 3): {
measure((1, 2, 3)):6d} bytes\”)
# __slots__ vs __dict__
print(\”\\n— __slots__ vs __dict__ —\”)
class WithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = [\”x\”, \”y\”]
def __init__(self, x, y):
self.x = x
self.y = y
d_obj = WithDict(1, 2)
s_obj = WithSlots(1, 2)
print(f\” __dict__ 对象: {
measure(d_obj):6d} bytes\”)
print(f\” __slots__ 对象: {
measure(s_obj):6d} bytes\”)
print(f\” 节省: {
measure(d_obj) – measure(s_obj):6d} bytes \”
f\”({
(1 – measure(s_obj) / measure(d_obj)) * 100:.0f}%)\”)
# 嵌套结构
print(\”\\n— 嵌套结构 —\”)
nested = {
\”users\”: [
{
\”id\”: 1, \”name\”: \”Alice\”, \”tags\”: [\”admin\”, \”dev\”]},
{
\”id\”: 2, \”name\”: \”Bob\”, \”tags\”: [\”user\”]},
],
\”config\”: {
\”debug\”: True, \”timeout\”: 30},
}
print(f\” 嵌套字典: {
measure(nested):6d} bytes\”)
# 循环引用
print(\”\\n— 循环引用(去重后) —\”)
a = [1, 2]
b = [3, a] # b 引用 a
a.append(b) # a 引用 b,形成循环
print(f\” 循环引用结构: {
measure(a):6d} bytes(只计算一次)\”)
if __name__ == \”__main__\”:
compare_memory()
print(\”\\n所有测试通过 ✓\”)
🔗 关联知识点
| sys.getsizeof() | 获取对象本身内存占用 |
| id() | 对象唯一标识,用于去重 |
| __dict__ vs __slots__ | 属性存储方式对比 |
| 递归遍历 | 处理嵌套结构 |
| 循环引用处理 | seen 集合防止无限递归 |
题目二:引用计数追踪器 ⭐⭐
📌 题目描述
实现一个引用计数追踪工具,能够显示任意对象在不同操作后的引用计数变化:
# 基础追踪
tracker = RefTracker()
a = []
tracker.track(a, \”创建 a\”)
b = a
tracker.track(a, \”b = a\”)
c = [a]
tracker.track(a, \”c = [a]\”)
del b
tracker.track(a, \”del b\”)
# 循环引用追踪
x = {
}
y = {
}
x[\”ref\”] = y
y[\”ref\”] = x
tracker.track_cycle(x, y)
# 输出报告
tracker.report()
💡 编程思路
这道题考察 引用计数机制的理解:
🖥️ 参考代码
import sys
from typing import Any, List, Dict
class RefTracker:
\”\”\”引用计数追踪器。\”\”\”
def __init__(self):
self.history: List[Dict[str, Any]] = []
def _get_refcount(self, obj: Any) –> int:
\”\”\”获取引用计数(减去 getrefcount 本身的临时引用)。\”\”\”
return sys.getrefcount(obj) – 1
def track(self, obj: Any, operation: str):
\”\”\”追踪一次操作后的引用计数。
Args:
obj: 要追踪的对象
operation: 操作描述
\”\”\”
count = self._get_refcount(obj)
self.history.append({
\”operation\”: operation,
\”refcount\”: count,
\”id\”: id(obj),
\”type\”: type(obj).__name__,
})
print(f\” [{
operation:20s}] refcount = {
count:2d} id={
id(obj) & 0xFFFF:04X}\”)
def track_cycle(self, a: Any, b: Any):
\”\”\”追踪循环引用的引用计数。\”\”\”
print(f\”\\n — 循环引用追踪 —\”)
print(f\” 创建 a = {
{}}, b = {
{}}\”)
self.track(a, \”创建 a\”)
self.track(b, \”创建 b\”)
a[\”ref\”] = b
print(f\” 执行 a[\’ref\’] = b\”)
self.track(a, \”a 添加 b 后\”)
self.track(b, \”b 被 a 引用后\”)
b[\”ref\”] = a
print(f\” 执行 b[\’ref\’] = a\”)
self.track(a, \”a 被 b 引用后\”)
self.track(b, \”b 添加 a 后\”)
print(f\”\\n 此时 a 和 b 互相引用,引用计数均为 2\”)
print(f\” 但外部已无任何变量指向它们(假设 del a, del b)\”)
print(f\” 引用计数不为 0,但对象已不可达 → 循环引用!\”)
def report(self):
\”\”\”打印完整追踪报告。\”\”\”
print(f\”\\n{
\’=\’ * 60}\”)
print(\”引用计数追踪报告\”)
print(f\”{
\’=\’ * 60}\”)
for i, record in enumerate(self.history, 1):
print(f\”{
i:2d}. {
record[\’operation\’]:25s} \”
f\”refcount={
record[\’refcount\’]:2d} \”
f\”[{
record[\’type\’]}]\”)
def demo_basic_tracking():
\”\”\”基础引用计数追踪演示。\”\”\”
print(\”=\” * 60)
print(\”=== 引用计数追踪 ===\”)
print(\”=\” * 60)
tracker = RefTracker()
# 创建对象
a = []
tracker.track(a, \”a = []\”)
# 赋值引用
b = a
tracker.track(a, \”b = a\”)
# 加入容器
c = [a]
tracker.track(a, \”c = [a]\”)
# 作为函数参数(临时引用)
def temp_ref(x):
tracker.track(x, \”函数内(参数)\”)
temp_ref(a)
tracker.track(a, \”函数返回后\”)
# 删除引用
del b
tracker.track(a, \”del b\”)
del c
tracker.track(a, \”del c\”)
tracker.report()
def demo_cycle_reference():
\”\”\”循环引用演示。\”\”\”
print(\”\\n\” + \”=\” * 60)
print(\”=== 循环引用演示 ===\”)
print(\”=\” * 60)
tracker = RefTracker()
# 创建循环引用
a = {
}
b = {
}
tracker.track_cycle(a, b)
# 验证 GC 可以回收
import gc
print(f\”\\n GC 前对象数: {
len(gc.get_objects())}\”)
del a, b
gc.collect()
print(f\” GC 后(循环引用已回收)\”)
def demo_small_int_cache():
\”\”\”小整数缓存演示。\”\”\”
print(\”\\n\” + \”=\” * 60)
print(\”=== 小整数缓存 ===\”)
print(\”=\” * 60)
tracker = RefTracker()
# 缓存范围内的整数
x = 100
y = 100
print(f\” x = 100, y = 100\”)
print(f\” x is y: {
x is y}(缓存命中)\”)
tracker.track(x, \”x = 100(缓存)\”)
tracker.track(y, \”y = 100(缓存)\”)
# 超出缓存范围
x = 1000
y = 1000
print(f\”\\n x = 1000, y = 1000\”)
print(f\” x is y: {
x is y}(缓存未命中)\”)
tracker.track(x, \”x = 1000(新对象)\”)
tracker.track(y, \”y = 1000(新对象)\”)
def demo_string_interning():
\”\”\”字符串驻留演示。\”\”\”
print(\”\\n\” + \”=\” * 60)
print(\”=== 字符串驻留 ===\”)
print(\”=\” * 60)
# 编译时驻留
s1 = \”hello\”
s2 = \”hello\”
print(f\” s1 = \’hello\’, s2 = \’hello\’\”)
print(f\” s1 is s2: {
s1 is s2}(编译时驻留)\”)
# 运行时拼接(不驻留)
s3 = \”hel\” + \”lo\” # 编译器优化后可能驻留
print(f\” s3 = \’hel\’ + \’lo\’\”)
print(f\” s1 is s3: {
s1 is s3}\”)
# 强制驻留
import sys
s4 = sys.intern(\”hello world! \” * 10)
s5 = sys.intern(\”hello world! \” * 10)
print(f\”\\n s4 = sys.intern(…), s5 = sys.intern(…)\”)
print(

