python 内核分析(三):对象的创建

本系列作为本人@takooctopus深入学习python机制的记录,这个博客遵照着栖迟于一丘的博客上面的流程进行的,也包含我在实际查看源码时的感想,特此列出,表示感谢。

对象的创建

Python在关于对象的创建上,提供了两种方式:

  • 1.C Api
  • 2.通过类型创建,比如PyLong_Type「Objects/longobject.c」

而在C Api中提供了两类:

  • 1.一种是泛型的Api,形式如同PyObject_Xxx「Include/object.h」,其能够应用在任何Python对象上。
  • 2.另一种是与数据类型相关的Api:

{{- code -}}

不管使用哪种模块,最终都是直接分配相应的内存。

对象的行为

我们继续去看PyTypeObject的定义,在「Include/object.h」中我们能够看见一个关于PyTypeObject的定义结构体:


{{- code -}}

在这整个结构体中,定义了许多函数指针,其指向了某个函数或者NULL,这些函数指针定义了Python类型对象的操作,其会在对象运行的状态而表现出不同的行为。

我们以上面11行的代码为例:


{{- code -}}

我们继续追踪,能在前面发现定义:


{{- code -}}

明显的,这个属性最终给出了一个函数指针。

对象流程

明显的tp_newtp_init定义了对象的创建于初始化操作。

而对于标准类有几个重要的方法:tp_as_numbertp_as_sequencetp_as_mapping

我们以第一个属性为例,追踪找到其表示的类PyNumberMethods


{{- code -}}

这一个结构体定义了当类型对象「PyTypeObject」被作为一个数值对象时所能够做的操作。

明显的nb_add代表了数值加法操作。

顺便的,我们可以得到另外两种类型的主要类型

  • tp_as_sequence对应list
  • tp_as_mapping对应dict

类型对象的类型

我们可以看见类型对象「PyTypeObject」的第一个属性就是PyObject_VAR_HEAD,这代表了这个Python对象的类型同时也是一个对象。

对其进行追踪后,我们发现其在「Include/object.h」中最开始定义了这个宏:


{{- code -}}

分别对应了一个可变对象以及基本对象「在上一篇中我们提到了基本对象」

而类型对象的类型就是它自己。

对象的多态性

对于一个Python对象来说,特别的对于Python基础对象「_object」「PyObject」来说,其有一个ob_type属性,其中包括了Python对象的类型信息。

这里的ob_type就是一个PyTypeObject即我们上面讨论的类型对象。

我们的Python对象能够直接使用箭头调用即object->ob_type->形式继续调用里面的属性

而函数之间一般通过泛型指针PyObject*来传递

我们回头看其中的某个属性,就拿C API中的所列出来的一样:


{{- code -}}

我们可以看到这个函数中进行了多态的判断。

对象的引用计数

在类似于C和C++以及Rust的语言中,程序员有极大的自由,能够任意的申请内存,其中的小问题可以看看这篇文章,但编程人员需要负责内存的释放问题,这其中很容易导致内存泄漏和空指针的BUG。

为了解决这些问题,现代高级语言大多采用了自己的内存管理和维护系统,像Java与C#均有其自己的垃圾回收机制。

同样的,Python也建立了其自己的垃圾回收机制。

最为明显的就是它的引用计数:

我们回去看与ob_refcnt的值变化有关的函数,可以在下面发现Py_INCREF(op)Py_DECREF(op)


{{- code -}}

我们可以看见当ob_refcnt这个引用计数减少到0时,Py_DECREF(op)就会调用析构函数_Py_Dealloc(_py_decref_tmp)进行内存的释放工作


{{- code -}}

一般来说如果Py_LIMITED_API没有被定义时,_Py_Dealloc(op)的触发会销毁对象,释放内存。

类型对象是超越引用计数规则的, 它永远不会被析构.并且类型对象是共享的, 多个int类型变量的类型对象指针都是指向同一个, 且不会被视为是类型对象的引用.

我们可以简单地验证一下


{{- code -}}

上面这段程序输出为TrueTrue

我们再看它们的id(x)id(y),在我的电脑上它们id均为22941395520,id是他们的身份唯一指定,我们也很容易看出其指向了同一个int对象。