Python源码解读之三 对象的创建

Python的速度问题

前面我们介绍了Python对象在底层的数据结构,我们知道Python底层通过PyObject和PyTypeObject完成了C++所提供的对象的多态性。在Python中创建一个对象,会分配内存并进行初始化,然后Python会用一个PyObject _来保存和维护这个对象,当然所有的对象都是如此。因为指针是可以相互转化的,所以变量在保存一个对象的指针时,会将该指针转成PyObject _ * 之后再交给变量保存。
因此在Python中,变量的传递(包括函数参数的传递)实际上传递的都是一个泛型指针:PyObject *。这个指针具体指向什么类型的对象我们并不知道,只能通过其内部的ob_type成员进行动态的判断,而正是因为这个ob_type,Python实现了多态机制。
比如:a.pop(),我们不知道这个 a 指向的对象到底是什么类型,但只要 a 可以调用 pop 方法即可,因此 a 可以是一个列表、也可以是一个字典、或者是我们实现了 pop 方法的自定义类的实例对象。所以如果 a 的 ob_type 是一个**PyList_Type ***,那么就调用 PyList_Type 中定义的 pop 操作;如果 a 的 ob_type 是一个 **PyDict_Type ***,那么就调用 PyDict_Type 中定义的 pop 操作。

所以变量 a 在不同的情况下,会表现出不同的行为,这正是 Python 多态的核心所在。
再比如列表,其内部的元素也都是 PyObject *,当我们通过索引获取到该指针进行操作的时候,也会先通过 ob_type 获取其类型指针,判断它的类型。然后再获取该操作对应的 C 一级的函数、进行执行,如果不支持相应的操作便会报错。所以操作容器内的某个元素,和操作一个变量并无本质上的区别。

从这里我们也能看出来 Python 为什么慢了,因为有相当一部分时间浪费在类型和属性的查找上面。
以变量 a + b 为例,这个 a 和 b 指向的对象可以是整数、浮点数、字符串、列表、元组、甚至是我们自己实现了 add 方法的类的实例对象。因为我们说 Python 中的变量都是一个 PyObject ,所以它可以指向任意的对象,因此 Python 就无法做基于类型方面的优化。
首先 Python 底层要通过 ob_type 判断变量指向的对象到底是什么类型,这在 C 的层面上至少需要一次属性查找。然后 Python 将每一个操作都抽象成了一个魔法方法,所以实例相加时要在类型对象中找到该方法对应的函数指针,这又是一次属性查找。找到了之后将 a、b 作为参数传递进去,这会发生一次函数调用,会将对象维护的值拿出来进行运算,然后根据相加的结果创建一个新的对象,再返回其对应的 PyObject * 指针
*。**
所以一个简单的加法运算,Python 在底层做了很多的工作,如果是放在一个循环中呢?那么上面的步骤要重复 N 次。
而对于 C 来讲,由于已经规定好了类型,所以 a + b 在编译之后就是一条简单的机器指令,因此两者在效率上差别很大。
当然我们不是来吐槽 Python 效率的问题,因为任何语言都擅长的一面和不擅长的一面,这里只是通过回顾前面的知识来解释为什么 Python 效率低。
因此当别人问你 Python 为什么效率低的时候,希望你能从这个角度来回答它,主要就两点:

  • Python 无法基于类型做优化
  • Python 所有的对象都存储在堆上

建议不要一上来就谈 GIL,那是在多线程情况下才需要考虑的问题。而且我相信大部分觉得 Python 慢的人,都不是因为 Python 无法利用多核才觉得 Python 慢的。
简单回顾了前面的内容,下面我们说一说 Python 的对象从创建到销毁的过程,了解一下对象的生命周期。不过由于这部分内容比较多,我们会分开说,先来看看对象是如何创建的。

Python / C API

当我们在控制台敲下这个语句, Python 内部是如何从无到有创建一个浮点对象的?

>>> pi = 3.14

另外 Python 又是怎么知道该如何将它打印到屏幕上面呢?

>>> print(e)
2.71

对象使用完毕时,Python 还要将其销毁,那么销毁的时机又该如何确定呢?带着这些问题,我们来探寻一个对象从创建到销毁整个生命周期中的行为表现,然后从中寻找答案。
不过在探寻对象的创建之前,需要先介绍Python提供的C API,也叫Python/C API。
Python 对外提供了 C API,让用户可以从 C 环境中与其交互。实际上,由于 Python 解释器是用 C 写成的,所以 Python 内部也在大量使用这些 C API。为了更好的研读源码,系统地了解这些 API 的组成结构是很有必要的,而 C API 分为两类,分别是泛型API和特型API。

泛型API

泛型API与类型无关,属于抽象对象层(Abstract Object Layer,AOL),这类 API 的第一个参数是*PyObject ,可以处理任意类型的对象,API 内部会根据对象的类型进行区别处理。而且泛型 API 名称也是有规律的,具有PyObject_Xxx这种形式。
以对象打印函数为例:

int PyObject_Print(PyObject *op, FILE *fp, int flags)

接口的第一个参数为待打印的对象指针,可以是任意类型的对象的指针,因此参数类型是PyObject *。而我们说PyObject *是Python底层的一个泛型指针,通过这个泛型指针来实现多态的机制。第二个参数是文件句柄,表示输出位置,默认是stdout,即控制台;而 flags 表示是要以 __str__打印还是以 __repr__打印。

// 假设有两个PyObject *, fo和lo
// fo指向 PyFloatObject, lo指向 PyLongObject
// 但是它们在打印的时候都可以调用这个相同的打印方法
PyObject_Print(fo, stdout, 0);
PyObject_Print(lo, stdout, 
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值