Python源码剖析之整数对象

專 欄

松直,Python中文社区专栏作者

专栏地址:

http://www.zhihu.com/people/songzhili?utm_source=qq&utm_medium=social

在《Python源码剖析》中,Python的版本为2.5,而在Python3中,前面提到,int类型的底层实现是Python2中的long类型。所以,我会在本章中,先介绍Python2源码中int类型的实现,再在最后介绍一下Python3.6中int(也就是以前的long)在底层的实现。之所以这样做的原因后面会解释。

在此之前,我们先来看一个有趣但没什么卵用的现象:

想知道为什么?看完这章就知道了。

初识PyIntObject对象

Python中对整数的概念的实现是通过PyIntObject来完成的。我们曾经说过它是一个定长对象,与之相对的还有变长对象。这样的分法对我们理解Python源码有帮助,但在Python语言的层面上,我们通常还使用一种二分法,即根据对象维护数据的可变性将对象分为可变对象(mutable)和不可变对象(immutable)。Python2中的PyIntObject是一个定长对象,而PyLongObject是一个变长对象,但它们都是不可变对象。也就是说,一旦创建了它们之后,就不能改变它们的值了。

我们来看一下在Python2.7中,PyIntObject的实现:

Python2中的整数对象PyIntObject实际上就是对C原生类型long的一个包装。

我们知道关于一个Python对象的大多数元信息是保存在它的类型对象中的,对于PyIntObject是PyInt_Type:

PyInt_Type中保存了关于PyIntObject的许多元信息,它定义了一个PyIntObject支持的操作,占用的内存大小等等。其中重要的内容我们会在下面叙述。

PyIntObject的创建和维护

创建对象的途径

在intobject.h中,Python提供了几种创建PyIntObject的途径:

事实上,前两种方法都是先将字符串转换为浮点数,再调用PyInt_FromFloat来实现的。

小整数对象

我们来思考一下,在Python内部,整数对象是如此广泛地被使用,尤其是那些比较小的整数。短短几秒之间,我们可能就要用的它们成千上万次,如果我们在创建它们的时候使用malloc来请求分配内存,删除它们时再调用free来释放内存。显然,这样的性能水平是不可能达到我们的要求的,而且也会造成极大的不必要的浪费。

于是,在Python内部,对于小整数使用了对象池技术。

我们可以看出,在Python2.7中,“小整数”的定义是[-5,256],而这个指向一个整数对象数组的指针small_ints就是这个对象池机制的核心。如果我们想要重新定义“小整数”怎么办?简单,我们可以修改源代码并重新编译。

对于小整数对象,Python直接把它们缓存在小整数对象池中,用于共享。那么大整数呢?肯定不可能都缓存在内存中,但是说不定某些大整数在某个时刻会变得十分常用,不过谁也不知道究竟是哪个数字。这时候,Python选择了另一种策略。

大整数对象

Python的设计者的策略是:对于小整数对象,直接把它们全部缓存在对象池中。对于其他整数,Python运行环境将会提供一块内存空间,这块内存空间由这些对象轮流使用。

在Python中,有一个PyIntBlock结构,它被用来实现这个机制。

PyIntBlock的单向列表通过block_list维护,每一个block中都维护了一个PyIntObject数组——objects,这就是真正用于存储被缓存的PyIntObject对象的内存。我们可以想象,在运行的某个时刻,这块内存中一定有一部分被使用,而有一部分是空闲的。这些空闲状态的内存需要被组织起来,以供Python在需要存储新的整数对象时使用。Python使用一个单向链表来管理全部block的objects中的空闲内存,这个链表的表头就是free_list。在一开始,block_list和free_list都指向NULL。

添加和删除

我们来看一下PyIntObject是如何从无到有地产生,又是如何消失的。

使用小整数对象池

如果NSMALLNEGINTS + NSMALLPOSINTS > 0成立,说明小整数对象池机制被激活了,然后Python会检查传入的long值是否是小整数。如果是,就直接返回对象池中的小整数对象就可以了。如果不是,那么会转向通用整数对象池。Python会在block的objects中寻找一块可用于存储新的PyIntObject的内存,这个任务需要free_list来完成。

创建通用整数对象池

显然,当首次调用PyInt_FromLong时,free_list必定为NULL,这时Python会调用fill_free_list创建新的block。在Python运行期间,只要所有block的空闲内存被使用完了,也就是free_list指向NULL,那么下一次调用PyInt_FromLong时就会再次激活对fill_free_list的调用。

在这个函数中,会首先申请一个新的PyIntBlock结构,这时block中的objects还只是一个PyIntObject对象数组。接下来,Python将objects中的所有PyIntObject对象通过指针依次连接起来从而将数组转变成一个单向链表。z在整个链接过程中,Python使用PyObject的ob_type指针作为连接指针。当链表转换完成后,free_list也就出现在出现在它该出现的位置了。从free_list开始,沿着ob_type指针,就可以遍历刚刚创建的所有为PyIntObject准备的内存了。

说完了PyIntObject的创建,我们再来看看它的删除。在Python中,当一个对象引用计数变为0 时,Python就会着手将这个对象销毁。不同类型的对象在销毁时执行的动作也是不同的,其在与对象对应的类型对象中被定义——也就是tp_dealloc。

首先,Python会检查传入的对象是否真的是一个PyIntObject对象,如果是的话,那么将其链入free_list所维护的自由内存链表中,以供将来别的PyIntObject使用。如果只是整数对象的派生类型,那么简单地调用派生类型中指定的tp_free。

使用通用整数对象池

在Python运行的过程中,会不只有一个PyIntBlock存在于同一个链表中,但是它们维护的objects却是分离的,之间没有联系。我们设想一下,有两个PyIntBlock对象,PyIntBlock1和PyIntBlock2,前者已经被填满,后者还有空闲的空间。所以此时free_list指向的是PyIntBlock2.objects中的空闲的内存块。当前者维护的objects中有PyIntObject被删除了,这时前者出现了一块空闲的内存。那么下次创建新的PyIntObject对象时应该使用这块空闲内存。否则就意味着所有的内存只能使用一次,这和内存泄漏没什么两样。

怎么才能将空闲的内存再交由Python使用呢?关键就在于前面我们分析的PyIntObject的删除操作,通过int_dealloc中的操作,所有的PyIntBlock的objects中的空闲内存块都被链接在一起了。它们形成了一个单向链表,表头正是free_list。

小整数对象池的初始化

现在关于Python的整数对象机制还剩最后一个问题。小整数对象池是在什么时候被初始化的呢?

我们可以看到,通过_PyInt_Init的调用,Python创建了这些小整数对象,然后它们就会在整个运行周期中一直存在,直至解释器毁灭。

Python3中int的实现

int即long

我们在之前提到,在Python3中int底层实现就是以前Python2中的long类型。空口无凭,我们来看代码:

我们可以看到,PyLong_Type类型对象的tp_name就是int,也就是说,在Python内部,它就是int类型。

之所以我们在一开始不介绍Python3中的整数实现,是因为在Python3中没有了通用的整数对象池(至少我没有找到),不过还保留着小整数对象池。同时对于那些比较小也就是对于之前Python2中的long类型大材小用的整数来说,也有一个更加快速且节省资源的创建方式。

而且我还发现了一个彩蛋,就是在longobject.c中第二行的注释:

哈哈哈,官方吐槽,最为致命。而且这个注释在Python2.7版本中也有,看来是一段陈年往事。

本文为作者原创作品,未经作者授权同意禁止转载

原文发布于微信公众号 - Python中文社区(python-china)

原文发表时间:2017-07-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏LuckQI

Java核心技术讲解六

1282
来自专栏程序猿

Linux sed 命令的使用

首先,就昨晚的发的消息道歉,虽然整蛊大家了,但是我还是挺开心的。 sed是一种流编辑器,配合正则表达式使用,sed处理文件之时,把当前处理的文保存...

40210
来自专栏信安之路

【作者投稿】奇葩webshell技巧

前段时间看XDCTF的一道web题,发现了一种很奇特的构造webshell的方法。

1310
来自专栏从流域到海域

《Java程序设计基础》 第3章手记

《Java程序设计基础》 第3章手记 本章主要内容: 1. 数据类型 2. 变量 3. 基本类型变量 4. 数据类型的转换规则 ...

2076
来自专栏程序员互动联盟

【编程基础】C++初学者需掌握的10个C++特性(中)

Strongly-typed enums 强类型枚举 传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域...

3324
来自专栏编程

Python原创0基础入门一看几张图就学会了

Python最近势头很猛,由于他在人工智能方面的出色表现,已经成为现在热门的编程语音之一,为了大家方便学习,而不是浪费时间去看入门教程,萌掌用图片的方式画出了你...

2248
来自专栏章鱼的慢慢技术路

牛客网_Go语言相关练习_判断&选择题(4)

错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况...

1212
来自专栏me的随笔

Python知识梳理

我们可以使用type()函数类获取对象的类型,Python3中内置数据类型包括:None,int,float,complex,str,list,dict,tup...

1522
来自专栏开发 & 算法杂谈

Coderbyte-Challenger之Letter Capitalize(单词字母大写)

Have the function LetterCapitalize(str) take the str parameter being passed and...

893
来自专栏JackieZheng

并发和多线程-八面玲珑的synchronized

上篇《并发和多线程-说说面试常考平时少用的volatile》主要介绍的是volatile的可见性、原子性等特性,同时也通过一些实例简单与synchronized...

1133

扫码关注云+社区

领取腾讯云代金券