![31bd0536ca45f0096736c4d06a0d9257.png](https://img-blog.csdnimg.cn/img_convert/31bd0536ca45f0096736c4d06a0d9257.png)
0 前言
Python 非常好用,哪怕一个没上过汇编,操作系统,编译原理等一系列基础计算机课程的人,也能快速上手。
再拿反面教材C++举例,引用和指针的区别入门阶段就搞懵了一批人。而指针和引用如果拓展开,C++老司机也是很容易翻车的。
Python好用的一个原因,就是把底层的很多复杂内容给封装简化了,当然很多动态语言也都再这么干(如PHP),只不过Python的用户体验大家一致觉得更好。
这个笔记系列,想从源码的角度来看,Python是如何把底层复杂内容进行封装的。
第一篇主要先讲大致框架,再拿int类型做一些展开。基于Python2.7的源码,Python3.0的源码会有区别,这个要注意。
1 万物皆对象,对象也为对象
先举个例子
Def
Python中variable可以为任何东西,int, dict, list,string,function。
对小白来讲,写函数不用考虑变量类型,学习和使用体验是很好的。(当然,在大型项目重构的时候,发现函数无法确定变量类型,返回类型,是很蛋疼的事情。所谓,动态一时爽,全家火葬场)
这种操作,C++中叫多态,而多态必须有一个共同的父亲节点。同理,Python底层C实现也是多态,都有一个共同的父类。
也就是,万物皆对象,对象也为对象。
1.1 背景知识——C中的多态实现方式
typedef
实现思路其实很简单,base class必须要是一个struct,继承类必须要在一开始就包含base struct。
1.2 对象三要素
对象三要素,引用计数,类型信息,类型内容。
这里从先从父亲节点说起,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。
而根据上文多态的定义,子类在一开始包含PyObject_HEAD即可继承PyObject对象。
1.2.1 引用计数——Py_ssize_t ob_refcnt
内存回收机制中的核心变量,引用计数,细节不展开。
1.2.2 类型对象——struct _typeobject *ob_type
Python中万物皆对象有多彻底呢?用来指定一个对象类型的类型变量也是一个对象。
[
PyTypeObject就是类型对象,继承了PyObject。
这个对象通过大量的函数指针和多态来定义了python对象所应该具有的内容。
1.2.3 类型内容
PyObject做为父类肯定没有类型内容,但子类,例如int子类,int内容放那呢?
[
很明显,在PyObject_HEAD后,加上了long变量来存储整数内容。
同理,list,dict,string也是如此设计,当然变长对象的设计会更复杂。
1.3 Python对象的多态
类型对象PyTypeObject通过函数指针加多态来实现,这里拿printfunc来举例。
[
PyTypeObject 定义printfunc的接口,因为PyIntObject是PyObject的子类,所以可以在intobject中实现这个接口。换成string,dict,set等对象实现原理也一样。
通过这三要素,PyObject已经把对象框架搭完毕。如果我们要实现一个int对象,根据PyObject中的类型中定义的接口,选择我们所需来实现即可。
2 int型对象分析
int对象的接口实现想对简单,但也是有不少有意思的点。
2.1 int对类型对象的接口实现
[
可以看到,并不是类型对象所有定义的接口,int对象都需要实现,赋值为0即代表不用实现。
上文已拿int_print讲过了,更多代码细节建议去看源码。
2.2 整数内存池
对C来讲,栈的内存申请和销毁速度要比堆快的多,为什么就不展开了。
C中的int,bool等build-in变量都是在栈上操作。Python中万物皆对象,也就是struct,新建的int对象要通过malloc在堆上申请。
这样速度必然要比C慢一大截,并且日常代码中,整数类型的使用是非常频繁的。
所以,Python就引入内存池和内存块来进行加速。
2.2.1 小整数对象内存池
PyIntObject是不可变对象,所以可以提前申请内存池来存储常用的小数字,直接从内存池来拿就可以使用。
问题是,多小的整数算小整数呢?Python是可以自定义的。
[
代码如上,注释也说的比较清晰。
2.2.2 大整数对象内存块
小整数对象通过固定的内存池解决了内存重复申请的问题。大整数对象是Python申请了一块固定的内存块,这些内存块由大整数轮流使用。
核心是两个链表指针,分成四步走
[
第一步,整数如果为小整数,则直接从小整数内存池中取。
第二步,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,再到虚拟机给写写,但抽空吧,可能又是三年后了呢。