Python 对象的内存模型与引用计数原理

4次阅读

id() 和 is 比较的是对象在内存中的唯一地址,而非值相等;同一对象多次赋值变量,id() 不变,a is b 为 True 当且仅当二者指向同一内存地址。

Python 对象的内存模型与引用计数原理

pythonid()is 到底在比什么

它比的不是“值相等”,而是对象在内存中的唯一地址。同一个对象,无论赋多少次变量,id() 都不变;两个变量指向同一块内存,a is b 才为 True

常见错误现象:用 == 误判引用关系,或以为 list.append(x)x 被“复制进去了”——其实只是存了对 x 的引用。

  • 小整数(-5 到 256)和短字符串会被缓存,所以 a = 100; b = 100; a is b 返回 True,但这属于实现细节,不可依赖
  • 自定义类实例、列表、字典等每次 new 都生成新地址,哪怕内容完全一样
  • 函数内返回局部列表,外部接收后仍指向原对象,修改会影响函数内“已结束”的上下文——因为没拷贝,只有引用

为什么 sys.getrefcount() 的结果总比预期多 1

调用 sys.getrefcount(obj) 这个动作本身,会让解释器临时持有一份引用(把 obj 压入 C 层参数),所以返回值恒为真实引用数 + 1。

使用场景有限:主要用于调试循环引用、确认对象是否被意外持有,不能靠它判断“变量是否还活着”。

立即学习Python免费学习笔记(深入)”;

  • 想测变量 x 的引用数?得写成 sys.getrefcount(x) - 1 才接近真实值
  • 传入字面量会触发临时对象创建,比如 sys.getrefcount(123) 返回的不是你想象中那个“123”的计数,而是临时 int 对象的计数
  • CPython 中,引用计数归零立即触发 __del__(如果定义了)和内存释放;但 PyPy、Jython 等不保证此行为

del 语句和变量赋值为 None 的区别在哪

del x 是从当前作用域命名空间里彻底移除名字 x,而 x = None 是把 x 指向另一个对象(None),原对象引用数只减 1,未必释放。

容易踩的坑:以为 x = None 就等于“清空”或“断开连接”,其实只要还有别的变量(比如类属性、闭包、全局字典里的键)指着它,对象就还在。

  • 对大对象(如 pandas DataFrame、numpy Array),del x 更干净,尤其在交互式环境反复加载时能防内存涨
  • del 不影响其他同名变量(比如嵌套作用域里的 x),它只删当前 scope 的绑定
  • 若对象被循环引用(如 A 持有 B,B 又持有 A),即使所有外部变量都 del 了,引用计数也不归零,得靠 gc 模块的周期性清理

什么时候引用计数不管用,必须靠 gc 模块

纯引用计数无法处理循环引用:A 引用 B,B 引用 A,外部再无其他引用,但两者引用计数都 ≥1,不会自动回收。

这是 CPython 的硬限制,不是 bug,是设计取舍。所有含 __del__ 方法的对象一旦卷入循环,就必须依赖 gc

  • 默认 gc 是开启的,但可被关闭(gc.disable())——关掉后循环引用对象永不回收,内存只增不减
  • gc.collect() 可手动触发,返回本次回收的对象数;调试时加 gc.set_debug(gc.DEBUG_STATS) 能看到每轮扫描详情
  • 避免在 __del__ 里操作可能被 gc 管理的全局结构(如往 list.append),否则可能引发“对象已被销毁”的静默失败

引用计数是 Python 内存管理的基座,但它只解决“单向依赖”的释放问题;真正让复杂对象图安全落地的,是那层看不见的 gc 循环检测逻辑——而它恰恰不参与日常编码的显式控制,只在后台默默兜底。

text=ZqhQzanResources