这是《Python源码剖析 — 深度探索动态语言核心技术》的阅读记录.
概述和内建对象的基石 - PyObject
我自认为我是没有能力可以完整的阅读并且理解这本书的, 因此可能不会完全覆盖这本书的全部内容, 并且理解可能会有偏差或者错误. 不过还是尽力而为吧, 初步将本篇阅读记录划成这么几个部分:
- Python的内建对象:
- 基石对象 — PyObject
- 整数
- 字符串
- List
- Dict
- …
- Python运行机制 — 虚拟机初探
- Python虚拟机控制流
- Python虚拟机的函数机制
- Python虚拟机的类机制
- Python模块加载机制
- Python多线程机制
- Python内存管理机制
后期在学习中再根据进度做修改.
当然源码是一定要下载下来的~我们来看下结构:
其中, Include
这样的命名大家都知道了, 是用来放头文件的. Lib
就是我们熟悉的那些Python标准库的源码了, 他们都是使用Python写的, 另外一个目录: Modules
同样也是放一些模块的, 只不过这里的都是使用C语言写的, 目的不言自明. Parser
就是Python解释器中的词法分析和语法分析, 其中还有些自动生成Python语言的词法和语法分析器, 类似YACC.Python
, 都用自己的名字来命名了, 这个目录就十分重要了, 包含了Python的编译器和执行引擎. 另外PCbuild
和PCbuild8
这两个玩意是VS的工程了, 由于我是在Mac上操作的, 所以就忽略掉了.
那就不说多了, 直接开始吧!
概述
先来撇清楚一个概念, 我们都知道Python是个面向对象的编程语言, 而这里内建对象的对象. 和面向对象的对象是两个截然不同的概念. 在Python中, 存在许多类型, 例如上面列举的int, string, List, Dict等等. 这些都实现了类的功能, 有意思的是, 这些类型, 他们也是类; 即是说: 类型本身也是类. 除了这些内置的, 我们还可以使用class A(object)
这样的语句进行自定类型的声明和创建相应的实例. 我们都知道Python是使用ANSI C实现的, 那么这些对象是怎么在C中表示的呢?
其实不仅仅是Python, 其他很多基于C实现的面向对象语言都是使用的结构体来存储一个类所包含的信息的. 也就是说, 所谓类, 也就无非是C中的结构体在堆上申请的一块内存. 在Python中, 对象的创建是按照固定的内存大小创建的. 但我们都知道我们是可以随意的给一个对象加上各种属性的, 这是怎么搞的?
其实是这样, 这些对象都在自己的固定内存中维护一个指向一个可变大小的内存区域的指针. 那你可能要问了, 为啥不直接制定可变大小的对象区域? 其实这个答案很简单, 如果是这样 那么连续创建的两个对象在前一个对象需要增加大小的时候, 第二个是需要移动的, 如果第二个后面还有其他的对象, 还需要继续移动下去, 为第一个对象提供内存空间, 这就很麻烦了.
PyObject
现在我们来看一下头文件中的object.h
, 他定义了Python对象机制中的核心对象 — PyObject
1 | typedef struct _object { |
而这其中, 藏了一个宏, 我们来看下具体内容是啥:
1 |
其中第一个宏, 向上追溯的话其实是一个DEBUG开关, 只有打开DEBUG的话才会有定义, 所以我们可以忽略掉. 而第二个Py_ssize_t
, 是Python为了更好的支持64位机器, 在2.5版本中进行的修改, 原本是int
, 这个ssize_t
, 学习C语言的同学应该是知道的, 在32位的机器上仍然是原本的int
, 而在64位的机器上等同于long int
.
那么搞到最后, 我们的PyObject其实就是这个样子啦:
1 | typedef struct _object { |
猜猜看, 这个int变量是干什么用的? 看名字大概可以猜出来的. 对, 这就是基于引用计数机制的内存回收实现了, 这个变量就是用来记录引用的, 当该变量变成0的时候, 变量就会从堆中删除.
另外一个呢? 叫做ob_type, 嗯 没错. 这个东西就是用来定义类型信息的, 标明对象的类型是什么.
接着我们先切换到另一个头文件:intobject.h
, 你会看到这个:
1 | typedef struct { |
除了包含一个PyObject
以外, 他还有一个long
值, 这个变量就是int的值保存的地方.
除此之外, 还有很多XXXobject.h
就定义了各种类型的Python类型对象, 各自包含了各自需要的属性.
int比较好办, 那么string呐. 我们知道int在C中是可以直接赋个值的, 但是string就不行了, 因为string在C中是数个char数组的表示, 那么维护一个字符串变量该怎么操作呢? 除此之外, 还有很多变量是类似string的, 他们都需要维护一个变长的属性, 例如List, Dict等等.
因此, 我们声明一个可变长Python对象: (回到object.h
)
1 | typedef struct { |
我们来看看这个宏展开是个啥:
1 |
其实就是一个PyObject再加上一个ob_size
, 也就相当于是一个拓展. 正如注释中说的那样, 这个变量是用来记录这个变长对象容纳了多少个元素, 即元素的个数.
现在你大概也就知道为啥我们把PyObject
叫做对象中的基石了, 因为不论是什么类型的变量, 他们的开头都是一个PyObject
头部, 也许你突然就知道了为啥宏叫做_HEAD
了, 对呀. 在Python内部, 所有的对象都拥有PyObject对象头部, 也就是说, 我们可以使用PyObject 指针引用任意对象.*
OK, 让我们继续. 一个很重要的问题, 对象是用来创建的, 创建就必然是需要进行内存的申请的 显然这个申请的空间大小是一个重要的元信息, 那它保存在哪里呢? 回过头来看看, 我们有一个部分还没看:
1 | typedef struct _object { |
是了, 就是那个_typeobject
. 这个东西的定义就可怕了, 是个85行的结构体, 截取一下:
1 | typedef struct _typeobject { |
通过注释, 我们大概可以了解到, 这个对象包含了大量的函数指针, 就上面截取的部分, 我们可以看到定义了类型的名字, 可以打印来进行内部的调试等, 另外还有用于分配内存的内存大小数值和元素大小数值.
其实呢, 这个巨大的PyTypeObject
就是Python中类的实现. 所以自然在这里就不展开了, 我们继续来看, 关于对象创建的概述.
对象的创建
当我们在Python中:
1 | int(10) a = |
这样便创建了一个值是10的int类型对象. 这个对象是怎么创建出来的?
实际上, Python有两种创建方式:
- C API - 范型API
- 类型相关API
对于第一种C API, 可以使用在任何Python对象上, 你只要调用就好, 内部会自动选择相应的函数. 创建一个对象就形如这样:
1 | PyObject* intObj = PyObject_New(PyObject, &PyInt_Type); |
而第二种就略有局限性, 面向类型. 所以就形如这样:
1 | PyObject* intObj = PyInt_FromLong(10); |
定义是这样的:
1 PyAPI_FUNC(PyObject *) PyInt_FromLong(long);
但是我们知道如果是自己声明的类, 同样可以进行创建, Python不可能准备用于创建用户自定义类的上述那种API, 那是怎么做到创建的呢?
在之前那个PyTypeObject
中, 定义了一个属性:
1 | struct _typeobject *tp_base; |
这个base类指针就指向当前这个类型的创建时参考的基类. 下面我们把创建对象会用到一些东西筛选出来用来说明:
1 | Py_ssize_t tp_basicsize |
首先会调用tp_new
这个创建函数, 如果是内置好的, 就会直接调用了. 但是如果是自定义, 这个函数指针实际上是NULL(在你也没有定义的时候), 这个时候就会去找tp_base
指针所指向的那个基类的tp_new
操作. 总之最后总会在object
中找到这个, 接着根据tp_basicsize
记录的信息进行内存的申请, 之后就会调用tp_init
来初始化对象.
关于这里我是用JavaScript的原型链来理解的, 理念上大概是一样的.
对象的行为/方法
另外, 在Python中整数int对象是可以进行加减乘除的, 显然这个功能应该是定义在里面的, 具体的位置在哪里呢? 我们可以在PyTypeObject
的定义中找到:
1 | /* Method suites for standard classes */ |
这是三个方法组, 要找计算相关, 想必是看第一个指针了, 我们来看下:
1 | typedef struct { |
喏, 这就是基本的加减乘除函数了. 另外我们知道Python中有很多魔术方法, 例如以前说过的:
1 | 'key'] a[ |
报错说:not subscriptable
. 这样的一个功能应该是字典才会具有的, 我们可以在PyMappingMethods *tp_as_mapping
中找到.
1 | typedef struct { |
这里的mp_subscript
就是了. 当我们使用__getitem__
这个特殊的方法的时候, 其实就是在指定这个mp_subscript
的行为. 也就是说, 为啥一个对象可以既具有数字还有字典的特性, 那就是因为Python在结构设计中同时加入了数字, 序列, 映射的方法组.
类型:类型
我们知道在Python中有个有趣的东西:
1 | type |
类型同样也有个类型, 这说明对象都有的类型本身也是一个对象, 其实刚刚在看_typeobject
的时候就已经知道了, 因为他同样也有一个可变对象头部: PyObject_VAR_HEAD
.
我们可以通过类型来描述一个对象是什么类型的, 但是如何描述一个类型的类型? 在typeobject.c
中就可以找到了:
1 | PyTypeObject PyType_Type = { |
喏, 这个PyTypeType
就是Python类型机制的核心了, 而在上面的<type 'type'>
这个就是这个对象的C实现了.
还是用int来做例子:
1 | PyTypeObject PyInt_Type = { |
我们发现, 最前面还是那个宏, 看起来玄机在这里了, 来找一下定义:
1 |
|
前面的Py_TRACE_REFS
是只有在DEBUG调试的时候才会被设置的FLAG. 也就是说引用计数默认是1, 类型始终是type.
那么现在就可以来稍微完整的来说一下了:
1 | a = int(10) |
一句简单的int
, 实际上是在创建PyIntObject
, 接着根据PyIntObject
所指向的类型:PyInt_Type
而我们知道这些内置的类型都是指向Object的, 也就是这货:
1 | PyTypeObject PyBaseObject_Type = { |
但是仍然, 无论是Int还是Object, 最后始终都是回归到PyType_Type
. 而PyType_Type
的ob_type
指针是指向自己的, 这就相当于是结束了指向过程.
我们一开始就说了, 所有的对象都是使用PyObject这个开头的, 也就是说, 一个PyObject类型的指针可以指向所有的对象, 但是去可以根据对象具体的属性来表现出不同的状态. 没错, 这就是多态性质的实现原理. 例如Hash
函数:
1 | long |
一个函数, 在传入不同的PyObject的时候, 表现也不一样, 这就是基于前面的多态实现的.
顺便说一下, 我们知道Python的垃圾回收机制是基于引用计数的, 那么作为重要的核心对象 - 类型对象. 自然是像root in Linux一样, 是不受到这个规则约束的. 他永远不会被析构.
至于关于引用计数的相关, 我们可以在object.h
中看到:
1 |
|
一些增加计数, 减少计数的宏.
至此, Python内建对象的概述就这样了 下一节就来认真看看我们本节中拿来用例子的IntObject
.