读 Python behind the scenes #6: how Python object system works

读 Python behind the scenes #6: how Python object system works

💡 原文中文,约6600字,阅读约需16分钟。
📝

内容提要

CPython 3.9中,所有对象都包含引用计数和类型指针。类型(PyTypeObject)通过tp_as_number等结构定义对象行为。以x+7为例,字节码BINARY_ADD调用PyNumber_Add,再经binary_op1选择左或右操作数的nb_add槽。内置类型如float静态定义float_add,用户类则通过slotdefs映射表将__add__关联到nb_add。

🔎

延伸解读

为什么内置类型不调用 __add__

文章指出,当 x 是内置类型如 int 或 float 时,x + 7 不会调用任何魔术方法。这是因为内置类型在 C 层面静态定义了 nb_add 槽,例如 float_add 直接操作 double 值。而用户类则通过 slotdefs 映射表将 __add__ 关联到 nb_add,运行时由 slot_nb_add 包装调用。理解这一区别有助于明白 Python 中内置类型与用户类在行为实现上的根本差异。

binary_op1 如何选择操作数实现

binary_op1 是决定调用左操作数还是右操作数 nb_add 的关键函数。它先获取左右类型的 nb_add 槽,若右类型是左类型的子类,则优先调用右操作数的实现;否则默认尝试左操作数,若返回 NotImplemented 再尝试右操作数。这一机制解释了为什么子类可以覆盖父类的运算符行为,也说明了 NotImplemented 在运算符重载中的实际作用。

字符串拼接的特殊优化路径

在 BINARY_ADD 字节码中,CPython 对两个精确的 str 对象做了特殊优化:直接调用 unicode_concatenate,而不是走通用的 PyNumber_Add。文章注释强调,不要试图用字节码微优化 int+int,因为对真实基准测试没有影响。这提醒我们,CPython 的性能优化往往针对特定场景,理解这些路径有助于避免无效的优化尝试。

从字节码到槽函数的完整链路

文章以 x + 7 为例,完整追踪了从字节码 BINARY_ADD 到最终执行的链路:BINARY_ADD 调用 PyNumber_Add,后者通过 binary_op1 定位到 nb_add 槽,再根据类型是内置还是用户定义,分别执行 float_add 或 slot_nb_add 包装的 __add__。这条链路展示了 CPython 对象系统的核心设计:类型定义行为,VM 通过槽函数动态分派。

❓

Q&A

Python 中所有对象在 CPython 底层都包含哪些基本字段?

所有 Python 对象都包含引用计数(ob_refcnt)和指向类型的指针(ob_type)。

CPython 中类型对象(PyTypeObject)是如何定义对象行为的?

类型对象通过 tp_as_number 等结构定义对象的行为,例如数值操作(加、减、乘等)由 PyNumberMethods 结构中的函数指针指定。

执行 x + 7 时,CPython 的字节码和函数调用流程是怎样的?

字节码 BINARY_ADD 会调用 PyNumber_Add,然后 PyNumber_Add 调用 binary_op1 来选择左或右操作数的 nb_add 槽。

binary_op1 如何决定调用左操作数还是右操作数的 nb_add?

binary_op1 首先检查左操作数的类型是否有 nb_add,如果有则尝试调用;如果左操作数返回 NotImplemented,则尝试右操作数的 nb_add。如果右操作数是左操作数的子类,则优先调用右操作数的实现。

内置类型(如 float)和用户自定义类在实现加法时有什么不同?

内置类型如 float 静态定义 float_add 函数,直接通过 tp_as_number 指向;用户自定义类则通过 slotdefs 映射表将 __add__ 方法关联到 nb_add 槽,运行时调用 slot_nb_add 来执行 __add__。

用户自定义类中的 __add__ 方法是如何与底层的 nb_add 槽关联的?

CPython 维护了一张 slotdefs 映射表,在类定义阶段将 __add__ 映射到 nb_add 槽,并设置对应的包装函数 slot_nb_add,运行时通过该槽调用 __add__。

🏷️

标签

➡️

继续阅读