人人都会AI编程

5.2 对象的底层结构:ob_refcnt、ob_type 与通用对象头

更新时间:2026-07-12

在 Python 中,整数、字符串、列表、函数、类,甚至类型本身都是对象。这些对象在 C 语言层面被实现为结构体,并且所有对象共享一个通用的“头部”,这个头部决定了对象的生命周期和行为。理解这个结构,是理解 Python 内存管理和动态类型的钥匙。

通用对象头:PyObject 和 PyVarObject

在 CPython 源码中,所有对象的基类型是 PyObject,它的定义大致如下(简化版):

typedef struct _object {
    Py_ssize_t ob_refcnt;   // 引用计数
    PyTypeObject *ob_type;  // 指向类型对象的指针
} PyObject;

对于长度可变的对象(如 liststrdict),还会增加一个字段表示元素个数,即 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 函数地址,然后调用它。
  • 内置的类型对象(如 intstr)也是由 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_INCREFPy_DECREF 来维护 ob_refcnt,否则要么内存泄漏,要么过早释放导致崩溃。

理解 PyObject 这个简单的头部,就像拿到了 Python 对象世界的“身份证”:它记录了对象的生死(引用计数)和身份(类型),也解释了很多表面上的语言行为。