Python 与内存管理,第1部分:对象
Source: Dev.to
值相等 – ==
== 检查两个对象是否包含相同的数据。当你写 a == b 时,Python 会比较对象中 存储的值。这可能涉及调用特殊方法,如 __eq__。
# == compares values
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True – same content
在内部,== 可能触发更复杂的操作:
- 对于列表,它会逐个比较元素。
- 对于自定义对象,它会调用对象的
__eq__方法。
class SensorValue:
def __init__(self, value, tolerance):
self.value = int(value)
self.tolerance = int(tolerance)
def __eq__(self, other):
# Equality within tolerance
return abs(self.value - other.value)
注意 (Python 3.14+) – 你可以通过
sys._is_immortal(obj)(私有 API)查询对象是否是永生的。
7. 回顾
| 运算符 / 函数 | 检查内容 | 常见使用场景 |
|---|---|---|
== | 值相等性(__eq__) | 比较数据 |
is | 身份(相同内存地址) | 单例检查(is None) |
id() | 对象的“地址”(整数) | 调试 / 反射 |
理解这些区别可以解释为什么:
- 小整数会被缓存,看起来是同一个对象。
- 大整数只有在显式绑定到多个名称时才会共享。
- 引用计数驱动内存回收,但不朽对象会绕过它。
掌握这些知识后,你可以编写更可预测、对内存更敏感的 Python 代码——并避免因混淆相等性和身份而产生的细微错误。
字符串驻留
Python 对某些字符串进行特殊优化:它 缓存 这些字符串,并将缓存的对象存储为 驻留 字符串。驻留字符串表现为“永生”对象——在解释器运行期间它们永不被释放。请参阅官方文档中的 sys.intern 描述。
注意 – Python 隐式地驻留许多字符串(例如标识符、短字面量)。因此,在比较看似相等但实际不是同一对象的字符串时,
is运算符可能会产生令人惊讶的结果。
小整数内存布局
id() 返回对象的内存地址。当我们查看连续小整数的地址时,会看到一个规律的模式:
for i in range(5):
print(f"id({i+1}) - id({i}) = {id(i+1) - id(i)}")
差值是 32 字节。这就是 Python 整数对象的大小吗?
import sys
print(f"{sys.getsizeof(1)=}") # sys.getsizeof(1)=28
sys.getsizeof(1)报告 28 字节 —— 对象数据的实际大小。- 当打印
id差值时看到的 32 字节间隔 来自 CPython 在 连续数组 中分配小整数的方式;每个槽位占用 32 字节,尽管对象本身只使用了 28 字节。
那 32 字节里都有什么?
| 偏移 | 大小 | 含义 |
|---|---|---|
| 0 – 7 | 8 B | 引用计数 (ob_refcnt)。对于“永生”的小整数,这里存放魔法值 0xC0000000。 |
| 8 – 15 | 8 B | 指向类型对象的指针 (ob_type)。 |
| 16 – 23 | 8 B | 大小字段 —— 组成整数的 digit(位)数量(Python 整数具有任意精度)。 |
| 24 – 27 | 4 B | 第一个 digit (ob_digit[0])。更大的数字会分配额外的 digit。 |
| 28 – 31 | 4 B | 为了 8 字节对齐而填充的空间。 |
CPython 对象布局(C 级别)
每个 Python 对象都以 PyObject 头部 开始:
typedef struct _object {
Py_ssize_t ob_refcnt; // reference count (or immortal flag)
PyTypeObject *ob_type; // pointer to type object
} PyObject;
整数(long)对象定义
在 CPython 源码(cpython/Include/cpython/longintrepr.h)中:
typedef struct _PyLongValue {
uintptr_t lv_tag; // number of digits, sign, flags
digit ob_digit[1]; // array of 30‑bit “digits” (uint32)
} _PyLongValue;
struct _longobject { // the public integer object
PyObject_HEAD // expands to the PyObject header above
_PyLongValue long_value; // the actual numeric data
};
该头部被内存管理器用于对象检索和垃圾回收。
引用计数
CPython 内存管理的核心是 引用计数。每个对象都保存一个计数器,用来记录有多少引用指向它。当计数器降至零时,内存会被释放。
你可以使用 sys.getrefcount() 查看引用计数(该函数会为你传入的参数额外添加 一个 引用)。
import sys
x = [1, 2, 3]
print(f"Initial refcount: {sys.getrefcount(x)=}") # 2 (x + argument)
y = x
print(f"Assigning y <- x: {sys.getrefcount(x)=}") # 3
z = x
print(f"Assigning z <- x: {sys.getrefcount(x)=}") # 4
del y
print(f"De‑assigning y: {sys.getrefcount(x)=}") # 3
z = None
print(f"De‑assigning z: {sys.getrefcount(x)=}") # 2
del x
print("Deleted x")
当引用计数降至 0 时,CPython 会立即释放该对象。
弱引用与循环引用
弱引用 不会 增加引用计数,即使仍然存在指向它们的弱引用,对象也可以被回收。
import sys
import weakref
class Node:
def __init__(self, child):
self.child = child
a = Node(None) # refcnt = 1
b = Node(None)
a_ref = weakref.ref(a) # weak reference, no refcnt increase
a.child = b
b.child = a # creates a cycle, refcnt = 2 for each
print(f"All refs a, b, and a_ref assigned: {sys.getrefcount(a_ref())=}") # 3 (2 + argument)
del a
print(f"After variable A deleted: {sys.getrefcount(a_ref())=}") # 3 (still alive because of the cycle)
del b
print(f"After variable B deleted: {sys.getrefcount(a_ref())=}") # 2 (object freed, but weakref still returns None)
为什么仅靠引用计数无法打破循环
当对象相互引用(形成循环)时,它们的引用计数永远不会降到零,导致内存泄漏。为此,CPython 添加了一个分代垃圾收集器,能够检测并回收此类循环。
代际垃圾收集器
收集器基于 代际假设:
- 大多数对象寿命短(约 80‑90 %)。
- 老对象很少引用年轻对象。
CPython 维护 三个代:
| 代 | 运行时机 | 描述 |
|---|---|---|
| 0 | 每 gc.get_threshold()[0] 次分配(默认 700) | 新对象。 |
| 1 | 每 gc.get_threshold()[1] 次第 0 代的收集(默认 10) | 在一次第 0 代收集后仍存活的对象。 |
| 2 | 每 gc.get_threshold()[2] 次第 1 代的收集(默认 10) | 长期存活的对象。 |
import gc
print(f"GC generations thresholds: {gc.get_threshold()}")
# (700, 10, 10) by default
由于第 0 代的收集只有在达到一定的分配次数后才会触发,刚创建的对象(或弱引用对象)即使所有强引用已经消失,也可能在短时间内仍然存活。这解释了上面示例中,删除 a 和 b 后,弱引用 a_ref() 仍然可以访问——循环垃圾尚未被收集。
TL;DR
- 字符串驻留 缓存特定字符串,使它们实际上是永久的。
- 小整数 存放在预分配的连续数组中;每个槽占用 32 字节,虽然对象本身只使用 28 字节。
- 每个 CPython 对象都以
PyObject头部开始(ob_refcnt+ob_type)。 - 引用计数 在计数降至零时自动释放对象。
- 弱引用 允许持有指向对象的非拥有指针。
- 循环引用 由 代际垃圾收集器 处理,收集器会根据分配阈值定期运行。