0 前言
Python 非常好用,哪怕一个没上过汇编,操作系统,编译原理等一系列基础计算机课程的人,也能快速上手。
再拿反面教材C++举例,引用和指针的区别入门阶段就搞懵了一批人。而指针和引用如果拓展开,C++老司机也是很容易翻车的。
Python好用的一个原因,就是把底层的很多复杂内容给封装简化了,当然很多动态语言也都再这么干(如PHP),只不过Python的用户体验大家一致觉得更好。
这个笔记系列,想从源码的角度来看,Python是如何把底层复杂内容进行封装的。
第一篇主要先讲大致框架,再拿int类型做一些展开。基于Python2.7的源码,Python3.0的源码会有区别,这个要注意。
1 万物皆对象,对象也为对象
先举个例子
Def test(variable):
Print type(variable)
Python中variable可以为任何东西,int, dict, list,string,function。
对小白来讲,写函数不用考虑变量类型,学习和使用体验是很好的。(当然,在大型项目重构的时候,发现函数无法确定变量类型,返回类型,是很蛋疼的事情。所谓,动态一时爽,全家火葬场)
这种操作,C++中叫多态,而多态必须有一个共同的父亲节点。同理,Python底层C实现也是多态,都有一个共同的父类。
也就是,万物皆对象,对象也为对象。
1.1 背景知识——C中的多态实现方式
typedef struct {
data member_x;
} base;
typedef struct {
struct base;
data member_y;
} derived;
void function_on_base(struct base * a); // here I can pass both pointers to derived and to basevoid function_on_derived(struct derived * b); // here I must pass a pointer to the derived class
实现思路其实很简单,base class必须要是一个struct,继承类必须要在一开始就包含base struct。
1.2 对象三要素
对象三要素,引用计数,类型信息,类型内容。
这里从先从父亲节点说起,PyObject定义如下
[object.h]
/* 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*.*/
typedef struct _object {
PyObject_HEAD
} PyObject;
#define PyObject_HEAD \_PyObject_HEAD_EXTRA \Py_ssize_t ob_refcnt; \struct _typeobject *ob_type;
注释已经说得很清楚,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。
而根据上文多态的定义,子类在一开始包含PyObject_HEAD即可继承PyObject对象。
1.2.1 引用计数——Py_ssize_t ob_refcnt
内存回收机制中的核心变量,引用计数,细节不展开。
1.2.2 类型对象——struct _typeobject *ob_type
Python中万物皆对象有多彻底呢?用来指定一个对象类型的类型变量也是一个对象。
[object.h]
typedef int (*printfunc)(PyObject *, FILE *, int);
typedef struct _typeobject {
PyObject_VAR_HEAD //根据宏定义,PyObject_VAR_HEAD即为PyObject const char *tp_name; /* For printing, in format "." */
printfunc tp_print;
//还有几十个函数指针,省略} PyTypeObject;
typedef struct {
PyObject_VAR_HEAD
} PyVarObject;
#define PyObject_VAR_HEAD \PyObject_HEAD \Py_ssize_t ob_size;/* Number of items in variable part */#define Py_INVALID_SIZE (Py_ssize_t)-1
PyTypeObject就是类型对象,继承了PyObject。
这个对象通过大量的函数指针和多态来定义了python对象所应该具有的内容。
1.2.3 类型内容
PyObject做为父类肯定没有类型内容,但子类,例如int子类,int内容放那呢?
[intobject.h]
typedef struct {
PyObject_HEAD
long ob_ival;
} PyIntObject;
很明显,在PyObject_HEAD后,加上了long变量来存储整数内容。
同理,list,dict,string也是如此设计,当然变长对象的设计会更复杂。
1.3 Python对象的多态
类型对象PyTypeObject通过函数指针加多态来实现,这里拿printfunc来举例。
[object.h]
typedef int (*printfunc)(PyObject *, FILE *, int);
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "." */
printfunc tp_print; //这里定义接口 //还有几十个函数指针,省略} PyTypeObject;
[intobject.c]
PyTypeObject PyInt_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int",
(printfunc)int_print, /* tp_print */
int_print(PyIntObject *v, FILE *fp, int flags) //在这里实现接口 /* flags -- not used but required by interface */
{
long int_val = v->ob_ival;
Py_BEGIN_ALLOW_THREADS
fprintf(fp, "%ld", int_val);
Py_END_ALLOW_THREADS
return 0;
}
PyTypeObject 定义printfunc的接口,因为PyIntObject是PyObject的子类,所以可以在intobject中实现这个接口。换成string,dict,set等对象实现原理也一样。
通过这三要素,PyObject已经把对象框架搭完毕。如果我们要实现一个int对象,根据PyObject中的类型中定义的接口,选择我们所需来实现即可。
2 int型对象分析
int对象的接口实现想对简单,但也是有不少有意思的点。
2.1 int对类型对象的接口实现
[intobject.c]
PyTypeObject PyInt_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int",
sizeof(PyIntObject),
0,
(destructor)int_dealloc, /* tp_dealloc */
(printfunc)int_print, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
(cmpfunc)int_compare, /* tp_compare */
(reprfunc)int_to_decimal_string, /* tp_repr */
&int_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)int_hash, /* tp_hash */
0, /* tp_call */
(reprfunc)int_to_decimal_string, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
Py_TPFLAGS_BASETYPE | Py_TPFLAGS_INT_SUBCLASS, /* tp_flags */
int_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
int_methods, /* tp_methods */
0, /* tp_members */
int_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
int_new, /* tp_new */
};
可以看到,并不是类型对象所有定义的接口,int对象都需要实现,赋值为0即代表不用实现。
上文已拿int_print讲过了,更多代码细节建议去看源码。
2.2 整数内存池
对C来讲,栈的内存申请和销毁速度要比堆快的多,为什么就不展开了。
C中的int,bool等build-in变量都是在栈上操作。Python中万物皆对象,也就是struct,新建的int对象要通过malloc在堆上申请。
这样速度必然要比C慢一大截,并且日常代码中,整数类型的使用是非常频繁的。
所以,Python就引入内存池和内存块来进行加速。
2.2.1 小整数对象内存池
PyIntObject是不可变对象,所以可以提前申请内存池来存储常用的小数字,直接从内存池来拿就可以使用。
问题是,多小的整数算小整数呢?Python是可以自定义的。
[intobject.c]
/* References to small integers are saved in this array so that theycan be shared.The integers that are saved are those in the range-NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
代码如上,注释也说的比较清晰。
2.2.2 大整数对象内存块
小整数对象通过固定的内存池解决了内存重复申请的问题。大整数对象是Python申请了一块固定的内存块,这些内存块由大整数轮流使用。
核心是两个链表指针,分成四步走
[intobject.c]
struct _intblock {
struct _intblock *next;
PyIntObject objects[N_INTOBJECTS];
};
typedef struct _intblock PyIntBlock;
static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;
第一步,整数如果为小整数,则直接从小整数内存池中取。
第二步,free_list如果不为null,则把free_list指向的空余内存分配给当前大数。
第三步,free_list如果为null,则申请一个PyIntBlock对象,一个PyIntBlock可以存多少个int对抗,量级可以自定义。
第四步,新申请的内存空间,用free_list串起来即可。具体参看intobject.c中的fill_free_list函数
还有两个关键步骤。
第一,Python是引用计数来释放内存,int类型内存释放后,free_list也要继续把这些free的内存串联起来。
第二,假如某个阶段int类型申请特别多,PyIntBlock自然也就申请了很多。然后某个阶段int被集中销毁,那么多个PyIntBlock是否完全保留,全都用free_list串起来?还是销毁大部分,只保留小部分?这块代码没细看。
这样的好处?
核心就一个,减少堆的碎片化。碎片化的坏处这里就不展开,Java中专门针对这个问题其实做了不少优化。
举个例子,C++的hash有一个内存碎片的问题,因为每个hash值指向的list都是用链表,链表的内存是分散的。对于超大的hash存储来讲,会导致堆的碎片化问题。
有一个优化的方式,就是让hash值指向的是个伪链表,实际上是个连续型内存。这个操作是不是看着跟上文的介绍有一点类似?
3 絮絮叨叨
这篇文章是基于三年前读《Python源码剖析》记的笔记,但一直没有完整的整理出来。
最近换工作,有了空闲时间,就花了几天时间整理了一下,还是挺有意思的。
毕竟身为策略工程师,天天用Python还是挺多的,对底层有一定了解还是挺好的。
后续的笔记自然就是继续把string,list,dict,再到虚拟机给写写,但抽空吧,可能又是三年后了呢。