Skip to main content

Chapter 8: Everything Is Object

Python 里任何可被绑定、传递、放入容器、返回给调用方的值,都要先落成运行时对象。读完本章后,读者应能把一个 Python 现象拆成三层判断:名字保存对象引用,对象带有身份和类型,类型决定后续操作的分发路径。

本章以 CPython 为主线。语言层保证每个对象有 identity、type 和 value;CPython 把这个模型落到 PyObject 头部、引用计数、类型指针、分配器和销毁路径上。这里的 CPython 细节用于解释默认解释器的行为,涉及 PyPy、MicroPython 或其它实现时,需要回到各自实现的对象模型重新判断。

贯穿本章的材料是一个短代码片段。它把整数、函数、类和实例放进同一个列表,观察点集中在 id()type()、绑定和生命周期上:

class Box:
pass

def build_box():
return Box()

number = 42
factory = build_box
kind = Box
instance = Box()
values = [number, factory, kind, instance]

for value in values:
print(id(value), type(value), value)

这段代码的目标是证明更底层的统一入口。42build_boxBoxBox() 的可调用性、可变性、属性集合各不相同;解释器面对它们时,都可以先拿到一个对象引用,再通过对象头部找到类型,并用类型决定后续操作。

8.1 PyObject identity and type pointer

对象身份(object identity)是运行时区分两个对象的稳定标识。Python 语言层通过 is 比较身份,通过 id() 暴露一个整数形式的身份值。身份属于对象本身,在对象存活期间保持稳定;对象销毁后,解释器可以复用底层内存,所以旧 id() 值不能作为跨生命周期的永久编号。

贯穿代码里,numberfactorykindinstance 都是名字。名字本身存放在某个 namespace 中,名字的值是对象引用。执行 values = [number, factory, kind, instance] 时,列表保存的是这四个对象的引用;列表没有把函数体、类定义或实例字段复制一份。

CPython 里,一个 Python 对象在 C 层通常表现为一个指向对象内存的指针。这个指针可以被当作 PyObject * 使用。PyObject 提供最小对象视角:解释器至少能从对象开头读到引用计数和类型指针。类型指针指向一个 PyTypeObject,它描述这个对象属于哪种类型,以及这种类型支持哪些操作。

下面这张图只表达本章需要的最小关系:名字指向对象,对象头部指向类型,类型再决定操作入口。

图中的 type-specific payload 是对象自己的数据区。整数对象会保存数值表示,函数对象会保存 code object、globals、defaults 等字段,类对象会保存 MRO、dict、slot 信息,实例对象会关联实例属性存储。它们的载荷不同,但开头的对象头部让解释器能先用统一方式识别“这是一个对象”。

type(value) 可以理解为从对象引用走到对象头部,再取得类型指针所指向的类型对象。Python 层看到的是一个类型对象,例如 <class 'int'><class '__main__.Box'>。CPython C 层使用 Py_TYPE(o) 取得对象的类型,官方 C API 文档也要求通过宏或函数访问这些字段,而非直接读写结构体成员。

身份和类型要分开判断。两个对象可以有相同 value,但身份不同,例如两个内容相等的列表。一个对象的类型决定它支持的协议入口,例如列表支持长度和下标访问,函数支持调用,类对象自身也支持调用并创建实例。Everything is object 的第一层含义,就是这些不同值都能先被解释器作为对象引用处理。

8.2 Object header, ob_refcnt, and ob_type

对象头部解决两个核心问题:对象还被多少地方持有,以及这个对象应按哪种类型解释。CPython 常规 release build 中,最小对象头部可以用下面的简化结构理解:

typedef struct _object {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;

这段 C 结构是简化模型,用于说明字段职责。真实 CPython 会通过宏、构建配置、调试配置和 ABI 约束组织对象结构。源码阅读时应优先看 Include/object.h 中的 PyObject_HEADPyVarObjectPy_TYPEPy_REFCNT 等入口,并结合官方文档 Common Object Structures 判断公共 C API 边界。

ob_refcnt 是引用计数状态。每当 CPython 获得一个新的强引用,引用计数增加;释放一个强引用,引用计数减少。当普通对象引用计数降到 0 时,解释器进入该对象类型的释放路径。这个字段让大部分对象可以在最后一个引用释放后立即销毁,也让 with open(...) 之外的许多临时对象能及时回收。

ob_type 是动态分发入口。Python 的 value + otherlen(value)、属性访问、调用、比较等操作,都需要先知道对象类型。类型对象里保存一批 slot 和方法表,解释器再根据操作种类选择对应入口。下一章会专门展开 PyTypeObject,本章只建立一个基本判断:对象头部的类型指针把每个值连接到一套类型级行为。

贯穿代码中的四个值可以放在同一个列表里,因为列表只保存对象引用。真正执行 type(value)callable(value)value.__class__ 时,解释器才根据每个对象的类型走不同路径。number 的类型指针指向 int 类型对象,factory 的类型指针指向 function 类型对象,kind 的类型指针指向 metaclass typeinstance 的类型指针指向 Box 类对象。

ob_refcntob_type 对应两个不同层级。引用计数管理对象存活,类型指针管理对象行为。把这两者合在一起看,就能解释很多 Python 现象:赋值通常改变名字到对象的引用关系,函数调用会创建和释放临时引用,操作符分发依赖对象类型,销毁路径由类型对象提供。

可变长对象还会引入 PyVarObject。它在 PyObject 的基础上增加 ob_size,用于元组、bytes、部分内部序列等带有长度概念的对象。这里要把 ob_size 和 Python 层的 len() 分开:ob_size 是对象内部表示的一部分,len() 是公开协议调用,后者可能经过类型 slot、方法或检查逻辑。

8.3 Object memory layout and allocation kind

对象内存布局(object memory layout)描述一个对象在内存中如何从头部延伸到类型专属字段。理解布局时先分三块:公共头部、可选的可变长头部、类型自己的数据。公共头部负责被解释器统一识别,类型数据负责表达具体对象的值和状态。

一个简化布局可以写成:PyObject header → optional size field → type-specific fields。对 int 来说,类型字段保存整数值的内部表示;对 list 来说,类型字段会指向元素指针数组并记录容量;对函数对象来说,类型字段保存 code object、globals、defaults、closure 等引用。不同对象的载荷差异很大,但解释器进入对象的第一步仍然是公共头部。

CPython 中常见对象还要区分分配种类。分配种类回答“对象内存来自哪里、何时释放、是否能被用户代码创建和修改类型对象”。本章使用下面的判断表:

分配种类典型对象生命周期判断工程边界
heap object普通实例、运行时创建的函数对象、用户定义类对象由分配器分配,靠引用计数和循环 GC 回收用户代码创建的大部分对象落在这里
static objectNone、部分内置类型对象、解释器静态定义的全局对象随解释器或运行时全局结构存在主要属于 CPython 内部实现
immortal objectCPython 3.12+ 中被标记为 immortal 的内部对象引用计数操作对它们可能无效果,常规 refcount 路径不触发销毁属于 CPython 内部优化和隔离支持的一部分

heap object 是最容易从 Python 代码触发的对象。执行 Box() 时,调用路径会进入类型对象的构造逻辑,分配一块适合 Box 实例的内存,初始化公共头部,再初始化实例状态。用户定义类 Box 自身也是对象,它通常是运行时创建的 heap type 对象,由 metaclass type 构造出来。

static object 常用于解释器自身需要长期持有的对象。None 在 C 层对应 _Py_NoneStruct,Python 代码通过 None 使用这个单例对象。内置类型对象也常以静态结构存在,例如 intlistobject 等类型对象由解释器启动时准备好。读取源码时,static object 的重点是“它也是对象”,但它的内存来源和释放时机与普通 heap object 不同。

immortal object 是 CPython 3.12 引入并持续演进的实现策略。它把一部分对象标记为引用计数不会走向 0 的对象,用于减少共享内部对象上的 refcount 写入,并服务未来多解释器、free-threaded 构建等方向。官方文档 Reference Counting 已经明确提醒,某些对象的 refcount 读数可能不反映真实引用数量;PEP 683 也说明 immortal object 是 CPython 内部特性。

这三类对象支持同一个语言层判断:它们都能被当作 Python 对象参与绑定、传递和类型查询。差异落在生命周期和内存来源上。读源码时看到 PyObject *,先确认对象头部和类型;继续追踪分配、释放、GC 或静态存储时,再判断它属于 heap、static 还是 immortal 路径。

8.4 Object allocation, destruction, and lifecycle

对象生命周期要从“创建对象引用”追到“最后一个强引用释放”。在 CPython 里,普通对象创建通常经过类型调用、内存分配、对象初始化、进入可达状态这几个阶段。对象销毁则从引用计数归零或循环 GC 发现不可达对象开始,进入类型定义的清理和释放路径。

instance = Box() 为例,Python 代码调用类对象 Box。类对象本身是 callable,它的调用逻辑会走 metaclass 的构造路径,最终分配实例内存、设置对象头部、关联 Box 类型、执行初始化。对用户来说这一行返回一个实例对象;对 CPython 来说,这一行完成了对象内存、头部、类型和实例状态的初始化。

普通构造路径可以概括为:

这张图只覆盖常规对象创建主干。内置对象、缓存对象、单例对象、不可变对象复用、扩展类型和 GC 对象会在主干上增加具体条件。源码阅读时不要把 PyObject_New 当成完整构造过程;官方 Allocating objects on the heap 明确说明,底层分配宏只分配并准备对象内存,完整对象构造应通过类型调用或类型 slot 完成。

引用变化贯穿对象生命周期。执行 factory = build_box 时,factory 增加了对函数对象的持有关系;执行 values = [...] 时,列表又持有这些对象。删除某个名字只会减少对应引用关系,列表里的引用仍然能让对象存活。判断一个对象是否会销毁,需要看整个引用图,而非只看单个名字是否还存在。

释放路径由类型负责收尾。当普通对象最后一个强引用释放后,CPython 调用对象类型的 deallocation 函数。释放过程可能清理对象内部持有的其它引用,可能执行 finalizer 相关逻辑,也可能把内存归还给对象分配器或类型自己的 free list。官方 Object Life Cycle 把这一过程拆成 tp_finalizetp_cleartp_dealloctp_free 等阶段。

循环引用会让生命周期多一个判断层。两个对象互相引用时,即使外部名字都删除,单纯引用计数也可能都大于 0。带有 GC 支持的容器对象会参与循环 GC,GC 通过遍历对象引用关系发现不可达的循环群组,再使用 tp_clear 打断内部引用。这个过程解释了为什么对象生命周期要同时看 refcount 和 GC 支持标志。

finalizer 会改变销毁路径的可预测性。对象清理过程中可能执行用户 Python 代码,例如 __del__ 对应的 finalization 路径。用户代码可以访问全局状态,也可能让对象重新变得可达。工程上判断析构相关问题时,要先让容器、全局表和对象内部状态处于一致状态,再释放强引用。

对象复用也属于生命周期边界。CPython 的分配器、interning、small integer 缓存、free list 等策略可能让两个时间点的对象使用相同地址。id() 在对象存活期间稳定;对象结束生命周期后,旧 identity 值的比较意义结束。把 id() 写入文件或网络请求中作为长期业务 ID,会把 CPython 的临时实现细节扩展到业务协议中。

8.5 Everything is object as a runtime rule

“一切皆对象”应当作为运行时判断规则使用:先确认 Python 代码中的值被表示为对象,再追踪这个对象的类型、引用关系、状态和操作分发。这个规则能统一解释普通值、函数、类、模块和异常对象,却不会抹平它们的能力差异。

函数是对象,所以 factory = build_box 只是多了一个名字绑定到同一个函数对象。函数对象内部持有 code object、globals、defaults、closure 等字段。调用函数时,解释器会通过 callable 协议和函数类型的调用路径创建 frame,绑定参数,执行 code object。

类是对象,所以 kind = Box 只是让名字 kind 指向同一个类对象。类对象的类型通常是 type,因此类对象也有自己的对象头部、引用计数和类型指针。调用 kind() 与调用 Box() 指向同一类对象的构造路径。后续 metaclass、MRO 和 descriptor 规则,都建立在“类对象也是运行时对象”这个判断上。

模块是对象,所以 import math 后名字 math 绑定到一个 module object。模块对象保存模块字典,函数的 __globals__ 也会指向对应模块 namespace。把模块作为对象理解后,动态修改模块属性、读取 sys.modules、重新绑定模块名等行为都能放进对象和 namespace 的关系中解释。

异常也是对象。raise ValueError("bad") 会创建或传播异常对象,异常对象携带类型、参数、traceback 关系和 chaining 状态。异常传播路径在 Chapter 6 已经出现,本章补上的底层连接是:异常处理器接收的是对象引用,异常匹配依赖异常对象的类型层级。

把贯穿代码重新整理成判断顺序,可以得到一个稳定模型:先看名字绑定到哪个对象,再看对象 identity 是否相同,再看对象类型指针指向哪个类型对象,再看类型对象提供哪些操作入口,最后看引用图决定对象能否释放。这个顺序比“Python 动态类型”更可执行,因为它把动态性落到了对象、类型和引用关系上。

这个模型也能解释工程中的常见现象。把函数放入列表、把类作为参数传给工厂、把模块存进注册表、把异常对象保存到日志结构,都是保存对象引用。性能成本可能来自对象分配、引用计数更新、类型分发、属性查找或 GC 扫描。排查时应先确定成本落在哪一层,再选择观察工具。

源码阅读时,本章对应的最小入口是 Include/object.hObjects/object.cObjects/typeobject.cObjects/obmalloc.cModules/gcmodule.c。读取顺序不需要从所有文件铺开。先用 PyObjectPyTypeObject 建立对象最小模型,再进入某个具体类型,例如 Objects/listobject.cObjects/funcobject.c,观察公共头部如何连接到类型专属字段。

最小自检任务

阅读下面代码,判断每一行改变的是名字绑定、对象引用关系、对象类型,还是对象内部状态。回答时按“名字 → 对象 identity → 类型 → 生命周期”的顺序说明。

class Counter:
def __init__(self):
self.value = 0

def inc(self):
self.value += 1

counter = Counter()
alias = counter
items = [counter, Counter, Counter.inc]
alias.inc()
del counter

答案要点

class Counter 执行后创建一个类对象,并把名字 Counter 绑定到这个类对象。这个类对象也有 identity 和 type;通常它的 type 是 type。类对象内部保存类 namespace、方法对象和后续实例创建所需的信息。

counter = Counter() 调用类对象,创建一个 Counter 实例对象。实例对象有独立 identity,类型指针指向 Counter 类对象。__init__ 给实例建立 value 属性,使对象内部状态出现 value = 0

alias = counter 改变名字绑定和引用关系。aliascounter 指向同一个实例对象,两个名字对应同一个 identity。这里没有创建第二个实例,也没有复制实例属性。

items = [counter, Counter, Counter.inc] 创建一个列表对象,并让列表保存三个对象引用:实例对象、类对象、函数描述符访问后得到的对象。列表自身也是对象,类型是 list。列表持有引用会延长其中对象的可达时间。

alias.inc() 通过实例对象的类型进入方法查找和调用路径,最终修改同一个实例对象的 value 属性。因为 aliascounter 指向同一对象,所以这个状态变化对所有指向该实例的引用可见。

del counter 删除名字 counter 在当前 namespace 中的绑定,并释放这条引用关系。实例对象仍然被 aliasitems[0] 引用,所以不会因为这一行立刻进入销毁路径。判断生命周期时要看整个引用图,而非只看某个名字是否存在。

本章知识点总结

  • 对象身份:identity 在对象存活期间稳定,is 比较 identity,id() 暴露实现相关的整数表示。
  • 名字绑定:名字保存对象引用,赋值通常改变 namespace 到对象的连接关系。
  • 统一头部:CPython 通过 PyObject 头部让不同对象具备统一识别入口。
  • 引用计数ob_refcnt 记录强引用持有状态,普通对象在最后一个强引用释放后进入释放路径。
  • 类型指针ob_type 指向类型对象,解释器依赖它选择操作分发入口。
  • 可变长头部PyVarObject 在公共头部上增加内部 size 字段,用于部分可变长对象。
  • 内存布局:对象布局由公共头部、可选 size 字段和类型专属字段组成。
  • heap object:普通实例和运行时创建的许多对象由分配器管理,生命周期依赖引用图和 GC。
  • static object:解释器静态持有的对象也遵守对象模型,但内存来源和释放时机不同。
  • immortal object:CPython 3.12+ 可以把部分内部对象标记为 immortal,使引用计数读数不能简单等同真实引用数量。
  • 构造路径:常规对象创建会经过类型调用、tp_newtp_alloc、头部初始化和 tp_init
  • 销毁路径:普通对象释放会进入类型提供的 tp_dealloctp_free,循环对象还可能经过 GC 的 tp_clear
  • 函数对象:函数可以绑定、传递和存储,调用时由函数类型的调用路径创建执行 frame。
  • 类对象:类本身是对象,实例创建是调用类对象并进入类型构造路径。
  • 判断顺序:分析 Python 运行时现象时,先看名字,再看 identity,再看 type,最后看引用图和生命周期。