欢迎光临
我们一直在努力

Python 内存管理与垃圾回收专项练习:6 道编程题从入门到精通

配套专栏: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__ 的差异

💡 编程思路

这道题考察 对象内存布局的理解 + 递归遍历:

  • sys.getsizeof():获取对象本身的内存占用(不含引用的子对象)。
  • 递归遍历:对于容器对象(list、dict、set、tuple、自定义实例),递归计算其引用的所有对象的内存。
  • 避免重复计算:使用 id() 去重,防止循环引用导致无限递归。
  • __slots__ vs __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()

    💡 编程思路

    这道题考察 引用计数机制的理解:

  • sys.getrefcount():获取对象的引用计数(注意:调用本身会临时增加 1)。
  • 追踪变化:记录每次操作前后的引用计数变化。
  • 循环引用检测:展示循环引用中引用计数不为 0 但对象已不可达的现象。
  • 格式化报告:清晰展示引用计数的变化轨迹。
  • 🖥️ 参考代码

    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(

    赞(0)
    未经允许不得转载:171主机测评 » Python 内存管理与垃圾回收专项练习:6 道编程题从入门到精通
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址