3

CPython 源码(一): PyObject - 纯纯的 Blog

 2 years ago
source link: https://blog.zhuangty.com/pyobject/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

CPython 源码(一): PyObject

CPy_

2017年7月30日 晚上

3.5k 字

29 分钟

这是 CPython 源码阅读系列的第一篇,我也不知道能坚持多久,或许连这篇都写不完(如果你能在网上看到这句话,说明至少第一篇写完了)。

Python 是一门非常强大的动态语言,语法特性多且优美,非常符合人类直觉,是我最喜欢的语言之一,再加上 CPython 做的优化非常少(笑),不像某些 JS 引擎如 V8,为了性能各种 hack 技巧太多不适合阅读学习。

CPython 源码链接

对 Python 比较精通以后,自然而然会产生疑问,那么强大的 Python,是怎么通过一门语法特性非常少的静态语言 C 来实现的呢?

当然,阅读 CPython 源码的第一步,就是让你抛弃 C 语言是静态类型的错觉。

本系列无明显顺序,更类似于个人阅读中的笔记,可能有错误,欢迎指出。

本系列文章要求阅读者:

  • 了解 C 语言的特性,特别是强制转换时的行为
  • 了解 Python 语言本身的基本行为和高级特性

作者开始阅读源码时,使用的 Python 版本是 Python 3.7.0a0,最新的 commit 号为 984eef7d6d78e1213d6ea99897343a5059a07c59

本文涉及的核心文件是

PyObject

一切都要从 Hello World 开始,CPython 源码阅读的 Hello World,当然是从 PyObject 开始。

首先,在 object.h 里找到 PyObject 的定义:

typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;

Python 中的一切对象,都至少保存了以上属性,这也是为什么在 Python 中,哪怕是一个简单的 0,也比 C 语言占用了更多内存的原因。

_PyObject_HEAD_EXTRA 这个宏是全空的,应该是为了未来可能的修改而保留修改空间,可以先忽略它。

观察一下 ob_refcntob_type

Python 是一门自带 GC 的语言,而 Python 的 GC 是以引用计数机制为主的,ob_refcnt 保存了 Python 每个对象被引用的次数。

object.c 中实现了一个函数 Py_IncRef,这个是暴露给 Python runtime embedders 的管理 PyObject 引用计数的接口,而源码内部的实现基本调用的是 Py_XINCREF(附录 0) 和 Py_INCREF 这两个宏。

/* Macros to use in case the object pointer may be NULL: */
#define Py_XINCREF(op) \
do { \
PyObject *_py_xincref_tmp = (PyObject *)(op); \
if (_py_xincref_tmp != NULL) \
Py_INCREF(_py_xincref_tmp); \
} while (0)

#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)

我们先假装没看到一系列的强制转换,那么这两个宏的作用就分别是 safe 和 unsafe 地增加一个 PyObject 的引用计数。

关于 ob_refcount 还定义了一些别的宏,具体的运用方式想放在 GC 的章节一起看。

ob_refcount 的类型是 Py_ssize_t,这个类型在我的系统上等价于 long,再加上为了为了效率,维护引用计数的时候显然不会作边界检查,这就意味着如果你的一个对象引用超过 LONG_MAX 的话应该会溢出,然而虽然很想尝试一下,但我并不知道怎么在爆 Memory Overflow 前让一个对象的引用计数超过 long

PyVarObject

ob_type 显然保存着对象的类型信息,然而在观察其具体实现之前,我们可以看一下紧跟着 PyObject 的另一个结构体的定义,PyVarObject

根据注释,PyVarObject 用来存储变长的 python 对象(如 list 等),熟悉 C++ 的同学可能已经忍不住脑补出以下代码:

// fake PyVarObject
class PyVarObject : public PyObject {
public:
Py_ssize_t ob_size;
};

那我们再来看它在 CPython 中的实现:

typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

看起来非常反人类的做法,难道每次想操作 PyVarObject 中 ob_type 的时候都要多一层 ob_base 吗?

然后就是一个比较神奇的操作,而且唯有 C 这种结构体严格映射内存结构的语言才能做到(附录 1),对于 C 来说,PyObject ob_base; 的内存结构和 _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; 是完全等价的,那么可以直接把 PyVarObject* 类型的对象强制转换成 PyObject* 类型的对象,然后直接当成 PyObject* 类型操作。

内存结构图

现在我们可以回过去看代码中对于 PyObject 某段注释:

Nothing is actually declared to be a PyObject, but every pointer to
a Python object can be cast to a PyObject*. This is inheritance built
by hand. Similarly every pointer to a variable-size Python object can,
in addition, be cast to PyVarObject*.

PyObject 不是 Python 中的任何一种类型,但是任何任何 Python 对象的指针都能被 cast 成 PyObject*,相当于手动实现了继承(附录 2)。同理如 PyVarObject*

为了方便之后 Python 中对象的定义,object.h 中定义了两个宏

#define PyObject_HEAD                   PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;

利用这两个宏来达到继承 PyObjectPyVarObject 的作用。

(看到这里,你可以再思考一下 C 究竟是不是静态语言,至少有没有被人当静态语言用)

为了获取 PyObjectPyVarObject 中的基础变量,object.h 定义了三个宏

#define Py_REFCNT(ob)           (((PyObject*)(ob))->ob_refcnt)
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
#define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)

任何 Python 中的对象都能通过这些宏来获取对应的信息来进行读写。

PyTypeObject

Python 中一切皆为对象,类型也不例外。

现在让我们回到被我们跳过的 ob_type,这是一个指向 PyTypeObject 对象的指针。

typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

// ...
};

PyTypeObject 继承了 PyVarObject,这个对象非常的复杂,在之后的文章再详细介绍。

[0]: 关于 do ... while(0)解释,之后的贴代码的时候可能会省略该部分。

[1]: 参见 https://stackoverflow.com/questions/2748995/c-struct-memory-layout

[2]: 虽然 C 语言里没有继承的语法,不过这个概念完全是继承,再加上作者钦定了,所以之后将直接用「继承了 PyObject」 这种语言来描述这种行为。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK