在 Python 中,整数、字符串、列表、函数、类,甚至类型本身都是对象。这些对象在 C 语言层面被实现为结构体,并且所有对象共享一个通用的“头部”,这个头部决定了对象的生命周期和行为。理解这个结构,是理解 Python 内存管理和动态类型的钥匙。
通用对象头:PyObject 和 PyVarObject
在 CPython 源码中,所有对象的基类型是 PyObject,它的定义大致如下(简化版):
typedef struct _object {
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 指向类型对象的指针
} PyObject;
对于长度可变的对象(如 list、str、dict),还会增加一个字段表示元素个数,即 PyVarObject:
typedef struct {
PyObject ob_base; // 包含 ob_refcnt 和 ob_type
Py_ssize_t ob_size; // 长度信息(如列表元素个数、字符串长度)
} PyVarObject;
每个 Python 对象,内存布局的起始部分都必然是这两个(或三个)字段。这意味着无论一个对象具体是什么类型,解释器都可以通过它起始的字节,直接找到它的引用计数和类型。
ob_refcnt:引用计数
- 定义:记录当前有多少个引用指向该对象。
- 增减规则:
- 当对象被一个新变量引用、放入容器、作为参数传递时,
ob_refcnt加 1。 - 当引用超出作用域、被
del删除、容器被销毁时,ob_refcnt减 1。 - 当
ob_refcnt变为 0 时,解释器立即释放该对象占用的内存(调用对象的释放函数)。 - 实际影响:
- 这是 Python 内存管理的主要机制,意味着大多数对象的生命周期是确定且即时的——一旦不再需要,马上被回收。
- 可以通过
sys.getrefcount()看到引用计数(注意函数参数本身会创建一个临时引用,所以返回值会比直观预期大 1)。
import sys
a = []
print(sys.getrefcount(a)) # 2(一个 a 变量,一个 getrefcount 的参数)
b = a
print(sys.getrefcount(a)) # 3(多了 b 的引用)
del b
print(sys.getrefcount(a)) # 2
- 存在的缺陷:单纯的引用计数无法处理循环引用(比如两个对象相互引用,但外部已无法访问它们)。这也是引入垃圾回收(标记-清除)算法的原因。
ob_type:类型指针
- 定义:指向一个
PyTypeObject类型的结构体,这个结构体描述了该对象的“类型”——所有该类对象共有的行为、方法、分配的魔法函数指针等。 - 如何决定行为:
- 当执行
obj + other时,解释器先通过obj->ob_type找到它的类型对象,再从中查找tp_as_number指向的add函数地址,然后调用它。 - 内置的类型对象(如
int、str)也是由PyTypeObject定义的,并且其ob_type又指向了更底层的元类型type。 - 实际效果:
- 变量无类型,对象有类型:赋值
x = 10时,x只是一个名字,类型信息储存在整数对象10的头部ob_type里。 type(obj)本质上就是返回obj->ob_type这个指针指向的类型对象。isinstance(obj, cls)就是沿着ob_type的继承链(通过tp_base)向上查找。
对象头带来的统一性
因为头部统一,CPython 在很多地方可以写出与具体对象类型无关的通用代码。例如:
- 内存分配与回收:分配器只关心对象的大小和引用计数,通过头部的
ob_type调用对应的析构函数。 - 调试和自省:
id(obj)返回的就是对象内存地址(通常又是PyObject的起始地址);dir(obj)可以遍历类型对象中记录的属性列表。 - 可变与不可变:整数是不可变的,因为
int类型没有提供修改内部值的方法;列表是可变的,其类型对象提供了setitem等修改操作的接口。这完全由类型对象控制,而对象头让解释器可以统一访问这些行为。
在代码中的直观感受
你不需要直接接触这两个字段,但很多日常现象都源于它们:
- 小整数缓存:整数
1只有一个对象,所有使用1的地方都引用同一个对象,其ob_refcnt很大,从而节省内存。 - 字符串驻留:相同的短字符串常量会被复用,引用计数很高。
- 函数参数的传递:传递的是对象的引用,所以被调函数内部修改可变对象,外部也会看到变化——因为大家操作的是同一个底层
PyObject,只是头部的引用计数变了。 - 手动管理引用:在 C 扩展中,你必须显式地
Py_INCREF和Py_DECREF来维护ob_refcnt,否则要么内存泄漏,要么过早释放导致崩溃。
理解 PyObject 这个简单的头部,就像拿到了 Python 对象世界的“身份证”:它记录了对象的生死(引用计数)和身份(类型),也解释了很多表面上的语言行为。