..
拆解 Python 对象模型

很多 Python 开发者写了很多年代码,但对 Python 的底层世界依然感觉雾里看花。

你是否思考过这些问题:

  • 为什么常说“Python 中一切皆对象”,连函数和类也是对象?
  • 为什么 Python 的变量不需要声明类型?
  • typeobject 到底是什么关系?为什么 type(object)type,而 object 又是 type 的父类?

如果不理解这些,你只是在用 Python 写 C 代码;理解了这些,你才能真正掌握 Python 的“动态之力”。今天,我们就深入 CPython 的源码层面,拆解 Python 的对象模型。


一、 底层解剖:PyObject 是万物之源

Python 的灵活性源于一个核心设计:所有东西在底层都是同一个结构体。

由于 CPython 是用 C 语言写的,当你创建一个整数 a = 10,或者定义一个函数 def func(): pass,在内存中它们并没有本质区别,它们都对应着 C 语言层面的一个结构体——PyObject

每一个 Python 对象,在内存头部都至少包含两个核心字段:

  1. ob_refcnt (引用计数):
    • 记录有多少个变量指向这个对象。当它变为 0 时,对象会被垃圾回收机制(GC)立即销毁。
  2. ob_type (类型指针):
    • 这是一个指针,指向该对象所属的类对象(Type Object)。
    • 比如整数 10ob_type 指向 int 类。这个指针告诉解释器:“我是一个整数,我支持加减乘除”。

结论: 无论外表多复杂,Python 对象的内核都是一个挂着“引用计数”和“类型标签”的 C 结构体。


二、 核心隐喻:变量是“便利贴”,不是“盒子”

理解对象模型的关键,在于纠正对“变量”的理解。

  • 在 C/Java 中: int a = 10; 就像申请了一个名字叫 a盒子,把数字 10 放进去。赋值 b = a 是把 10 复制一份放到 b 盒子里。
  • 在 Python 中: a = 10 就像在内存里吹起了一个气球(对象 10),然后拿一张写着 a便利贴(变量名)贴在气球上。
    • 当你执行 b = a 时,不是复制气球,而是拿一张写着 b 的便利贴,贴在同一个气球上。

这就是为什么 Python 的参数传递全是引用传递(Pass by Assignment)。这也解释了 Python 的“三位一体”特性,任何对象都有:

  1. Identity(身份): 内存地址(id(obj))。
  2. Type(类型): 它的模具是哪个类(type(obj))。
  3. Value(值): 气球里的内容。

三、 终极烧脑:type 和 object 的“鸡蛋悖论”

Python 对象模型中最令人困惑,也最精妙的设计,莫过于 typeobject 的关系。它们构成了对象系统的时空闭环。

3.1 两个主角

  • object(万物之祖): 它是继承链的终点。所有的类(int, str, MyClass)默认都继承自它。它定义了对象最基本的行为(如 __hash__)。
  • type(万物之主): 它是实例化链的源头。也就是所谓的“元类”(Metaclass)。所有的类(包括 object)本质上都是 type 创建出来的实例。

3.2 只有两句话是真的

如果你被绕晕了,只需要记住这两句“绝对真理”:

  1. typeobject 的子类。 (继承维度:type 也是个类,所以它得认 object 做父类)
  2. objecttype 的实例。 (实例化维度:object 这个类对象,是由 type 制造出来的)
print(issubclass(type, object))  # True
print(isinstance(object, type))  # True
print(isinstance(type, type))    # True (自己造自己)

3.3 源码揭秘:C 语言层面的神级操作

你可能会问:这逻辑不通啊?如果是 type 造了 object,那在 type 诞生之前 object 应该不存在;但 type 又继承自 object,说明 type 诞生前 object 必须存在。这不就是死锁了吗?

在 C 语言实现的底层(CPython 源码),开发者通过精妙的指针操作解决了这个“先有鸡还是先有蛋”的问题。这是一个人工打破死循环的过程:

  1. 先定义结构体: C 语言代码中,先静态定义了两个核心结构体:
    • PyType_Type(对应 Python 里的 type
    • PyBaseObject_Type(对应 Python 里的 object
  2. 手动连接(Bootstrap): 此时它们还只是孤立的 C 结构体,编译器无法处理这种互相依赖。于是,CPython 在初始化时进行了“手动硬连线”:
    • 让 type 成为自己的实例:PyType_Typeob_type 指针指向它自己(&PyType_Type)。
    • 让 type 继承 object:PyType_Typetp_base 指针指向 PyBaseObject_Type
    • 让 object 成为 type 的实例:PyBaseObject_Typeob_type 指针指向 PyType_Type

这种“我指你,你指我,我自己指我自己”的操作,在 C 语言层面完美闭合了逻辑环。

3.4 为什么这么设计?

这种看似复杂的环形设计,实际上是为了保证 Python 对象模型的一致性

  • 没有特例: 在 Python 中,一切皆对象。既然 typeobject 也是对象,它们就必须遵守对象的规则(有类型、有父类)。
  • 逻辑闭环: 通过让两者互为依托,Python 关闭了对象系统的顶层逻辑。这确保了无论你在系统中怎么回溯,永远不会遇到一个“不是对象”的东西。

3.5 形象类比

  • object 就像是“塑料”这种材质。
  • type 就像是“制造模具的机器”。
  • 源码层面的操作: 工程师先用手捏了一个“最初的机器”(静态定义的结构体),然后用这台机器造出了所有后续的模具,最后甚至给这台机器贴上了“塑料制造”的标签。

四、 动态机制:类亦是对象与属性查找

基于上述模型,Python 衍生出了极具动态特性的行为。

1. 类也是对象(First-class Citizen)

在 Python 中,class Dog: 这行代码执行完后,内存里真真切切地产生了一个名为 Dog 的对象。 正因为类是对象,所以:

  • 你可以把类赋值给变量。
  • 你可以把类当参数传给函数。
  • 你可以在运行时动态修改类的属性(Monkey Patching)。

2. 属性查找(Attribute Lookup)

当你敲下 obj.x 时,Python 不会像 C++ 那样去偏移内存地址,而是启动了一次哈希查找

  1. 先去 obj.__dict__(实例字典)里找。
  2. 没找到?去 obj.__class__.__dict__(类字典)里找。
  3. 还没找到?顺着 MRO(方法解析顺序)去父类字典里找。
  4. 实在没有?调用 __getattr__ 给你最后一次机会。

这种机制虽然比指针偏移慢,但它带来了无与伦比的灵活性。


五、 总结

Python 的对象模型是一种用空间(内存)和时间(速度)换取极致灵活性的艺术。

  • 统一性: 无论是整数、函数还是类,众生平等,皆为对象。
  • 元编程: 通过控制 type(元类),你可以控制类的创建过程,这是 Django ORM 等黑魔法的基石。
  • 自洽性: 正是 C 语言底层那一次“精妙的指针连接”,让 typeobject 互为支撑,构建了一个逻辑完美自洽的动态世界。

当你下次写下 class MyClass 时,希望你能意识到:你不仅仅是在写代码,你是在指挥 type 这位造物主,用 object 这种基底材质,为你创造一个新的世界。