cpython 源码概览
跟着《Python 源码剖析》,用空闲时间断断续续读了 cpython 的一部分源码,大致了解了 cpython 解释器是怎么做的。对一些关键部分做一些整理和总结。其实想写这篇很久了,一直拖延….
概述
用的源码版本是 2.7.14,可以从 python.org 下到。CPython 源码整体可以分成 3 个部分:Python runtime, 包含对象/类型系统、内存管理、运行时状态信息;Python 解释器,编译器前端和字节码解释器;各种模块、库。编译器前端就不太关心了,反正总有办法得到 AST 的,编译部分也不太关心(怎么得到字节码)。主要关心 Python 的运行时环境,即解释器是怎么执行代码, Python runtime 是怎么运作的,以及 Python 内部的各种机制是怎么产生的,基础数据结构是怎么实现的。
关于阅读源码的意义:其实说到底对写 Python 的人而言没有太大意义,但是了解基础数据结构的实现、对象/类型系统的实现都有助于少踩坑。再有就是能够了解动态语言的一些实现方法,读读 C 代码,看一些 C 的技巧也不是什么坏事。
其实写 Py 也越来越少了,很多东西也迟早会忘记的,最近一年对 Py 的感受就是,发展越来越奇怪,看不懂。被各种机器学习、深度学习等高大上的东西带着吹,Python3 都在折腾些稀奇古怪的 “优雅/牛逼” 的特性,正经的性能啥的基本没有改善。我觉得一个成熟的语言加各种新特性不是必要的,而且特性越来越多就显得太繁杂了,并不是啥好事。比如在 Py 里加 pattern matching 感觉就没有意义,也就只发挥个语法糖的作用。
对象初步
Python 中绝大多数东西都是对象 (Object),在 C 本身没有 OO 一说,但是 C 有办法写出 generic 的代码。 Python 里的对象在 C 里定义成这样:
1 | /* [object.h] */ |
只要一个结构体的第一个 filed 是
PyObject_HEAD
,通过这个宏就会插入相应的对象头部,那么无论这个结构体后面是什么,它其实都可以被当做一个 PyObject
使用(通过 PyObject *
)。理解这个是继续阅读 Python 源码的基础头部插入 PyObject_VAR_HEAD
的结构体是不定长对象,它只比普通对象多了一个 ob_size
,这表示对象的元素个数(当然,有时出于各种原因,这个字段可能被当其他用途)。注意这个不定长指的是所需要内存在运行时确定,比如整数当然是定长的对象,字符串是不定长的对象,ob_size
表示字符个数。不定长对象也有可变、不可变,比如字符串是不可变的,list 是可变的。
PyObject_HEAD
里的 ob_refcnt
是对象的引用计数,归零的时候需要回收内存,ob_type
是 struct _typeobject
的指针,这表示这个对象的类型,比如 'abc'
是个字符串对象,这个对象的 ob_type
会指向 PyStringObject
对象(即 Python 中的 str
)。类型对象都是 struct _typeobject
:
1 | /* [object.h] */ |
如前,PyTypeObject
里包含了这个类型的信息,以及这个类型的各种行为(各种函数指针或者包装过的函数指针)。
在 Python 里,type('abc') == str
,即 'abc'
是 str
构造而来, ob_type
指向 str
。而 type(str) == type
,即 str
由 type
构造而来,这里的 type
就是类型的类型(在 OO 语言里类型一般和类重合,可以把 Py 里能实例化出类型的类叫做元类)。 在源码里,type
实际上是 PyType_Type
:
1 | /* [typeobject.c] */ |
部分数据结构的实现
整数
这里的整数主要是指 PyIntObject
,而不是 PyLongObject
,后者是指大整形,类似 JAVA 里的 BigInteger。
1 | /* intobject.h */ |
整数的数据结构比较简单,只在 PyIntObject
里用一个 long 来存数值。对应的 type 是 PyInt_Type
。整数是很简单的实现,主要特点是实现了一个小整数内存池,把一定数值范围内的整数对象提前创建,往后用到这个数据范围内的数值直接从内存池里取出,默认范围是 [-5, 257)。
1 |
|
这个 small_ints
在解释器初始化的时候进行初始化。对于小整数范围之外的整数,Python 也维护了一个内存池,回收整数对象的时候,并不回收 PyIntObject
,而是插入到内存池里以供下次使用(避免了重新分配对象)。具体实现就是引入一个 IntBlock 对象,然后用两个链表维护,复用空闲对象的 ob_type 字段之类的。总体感觉,永远不回收内存,无穷大的内存池,不是很好。
字符串
字符串在 Python 中实现为 不可变且不定长 对象,不定长是指这个对象在运行时占用的内存不确定(根据字符串长度不同而不同),不可变是指字符串对象无法被修改(修改某个字符或在末尾添加字符或删除字符)。
1 | /* */ |
Python 把 VAR_HEAD 里的 ob_size
用来记录字符个数。字符串不算很复杂,接口有 PyObject * PyString_FromString(const char *str)
,通过 C 字符串创建 Python 字符串。注意 Python 有 Intern 机制。Intern 机制的思想是,对于部分相同的字符串,没有必要在内存里维护多个副本。Python 实现的比较搓,本质上是拿一个字典,key 和 value 都是被 intern 的字符串,每次 PyString_InternInPlace
的时候根据字符串对象到字典里去找,如果找到了,直接回收传入的字符串,并把那个传入的字符串的指针修改成被 Intern 过的字符串。这就意味着,无论如何,都会先创建一个临时的字符串对象。
PyString_FromString
里只对长度为0和1的字符串进行 Intern,实际上在其他的代码里,会在 PyString_FromString
之后再根据情况调用一下 PyString_InternInPlace
。具体的一些处理方法可以参考 这篇帖子:
该机制规定:当两个或以上的字符串变量它们的值相同且仅由数字字母下划线构成而且长度在20个字符以内,或者值仅含有一个字符时,内存空间中只创建一个对象来让这些变量都指向该内存地址。当字符串不满足该条件时,相同值的字符串变量在创建时都会申请一个新的内存地址来保存值。
另外,并非全部的字符串都会采用intern机制。仅仅包括下划线、数字、字母的字符串才会被intern。也就是说。仅仅对那些看起来像是python标识符的进行intern。
另外注意字符串的加法运算,因为字符串不可变,对于长度为 n, m 的两个字符串相加,Python 会构造一个长度为 n+m 的新字符串。这意味着好几个字符串用连续加法拼到一起,复杂度是炸裂的。所以推荐使用线性的 join 操作。可以参考 stringobject.c 下面的 string_join
和 string_concat
。
列表
tbc
字典
类和对象机制
内存管理机制
Author: shengrang
Link: https://blog.runc.dev/2018/10/06/py-source/
License: 知识共享署名-非商业性使用 4.0 国际许可协议