Python对象系统如何工作

2020-12-12 01:56:01

从本系列前面的部分我们知道,Python程序的执行包括两个主要步骤:

我们已经将重点放在第二步已经有一段时间了。在第4部分中,我们研究了评估循环,即执行Python字节码的地方。在第5部分中,我们研究了VM如何执行用于实现变量的指令。我们还没有讨论虚拟机实际上是如何计算某些东西的。我们推迟这个问题是因为要回答这个问题,我们首先需要了解语言最基本的部分是如何工作的。今天,我们将研究Python对象系统。

注意:在本文中,我指的是CPython 3.9。随着CPython的发展,某些实现细节肯定会发生变化。我将尝试跟踪重要的更改并添加更新说明。

为了计算函数f,CPython必须计算表达式x +7。我想问的问题是:CPython如何做到这一点?您可能会想到诸如__add __()和__radd __()之类的特殊方法。当我们在类上定义这些方法时,可以使用+运算符添加该类的实例。因此,您可能会认为CPython会执行以下操作:

如果x没有__add __(),或者此方法失败,它将调用(7).__ radd __(x)或int .__ radd __(7,x)。

艰难的现实要复杂得多。实际发生的情况取决于x是多少。例如,如果x是用户定义类的实例,则上述算法类似于事实。但是,如果x是内置类型的实例(例如int或float),则CPython根本不会调用任何特殊方法。

让我们将此算法应用于函数f。编译器将此函数的主体转换为以下字节码:

$ python -m dis f.py ... 2 0 LOAD_FAST 0(x)2 LOAD_CONST 1(7)4 BINARY_ADD 6 RETURN_VALUE

BINARY_ADD从堆栈中弹出两个值,将它们相加并将结果推回堆栈中。

VM如何将两个值相加?要回答这个问题,我们需要了解这些值是什么。对我们来说,7是int的实例,而x是任何东西。但是,对于VM,一切都是Python对象。 VM推送到堆栈并从堆栈弹出的所有值都是指向PyObject结构的指针(因此,短语" Python中的所有内容都是对象)。

VM不需要知道如何添加整数或字符串,即如何进行算术或连接序列。它需要知道的是每个Python对象都有一个类型。反过来,类型知道有关其对象的所有信息。例如,int类型知道如何添加整数,而float类型则知道如何添加浮点数。因此,VM要求类型执行操作。

这种简化的说明抓住了解决方案的本质,但同时也省略了许多重要的细节。为了获得更真实的画面,我们需要了解什么是真正的Python对象和类型以及它们如何工作。

我们已经在第3部分中讨论了Python对象。在这里值得重复讨论。

typedef struct _object {_PyObject_HEAD_EXTRA //宏,仅用于调试目的Py_ssize_t ob_refcnt; PyTypeObject * ob_type; } PyObject;

我们说过,VM将任何Python对象都视为PyObject。那怎么可能? C编程语言没有类和继承的概念。不过,可以在C中实现可以称为单一继承的东西。 C标准指出,指向任何结构的指针都可以转换为指向其第一个成员的指针,反之亦然。因此,我们可以"扩展"通过定义第一个成员为PyObject的新结构来实现PyObject。

浮点对象存储PyObject存储的所有内容以及浮点值ob_fval。 C标准仅声明我们可以将指向PyFloatObject的指针转换为指向PyObject的指针,反之亦然:

VM之所以将每个Python对象都视为PyObject,是因为它需要访问的只是对象的类型。类型也是Python对象,是PyTypeObject结构的实例:

// PyTypeObject是" struct _typeobject"的typedef struct _typeobject {PyVarObject ob_base; //扩展PyObject_VAR_HEAD宏const char * tp_name; / *对于打印,格式为"< module>。< name>" * / Py_ssize_t tp_basicsize,tp_itemsize; / *对于分配* / / *实现标准操作的方法* /析构函数tp_dealloc; Py_ssize_t tp_vectorcall_offset; getattrfunc tp_getattr; setattrfunc tp_setattr; PyAsyncMethods * tp_as_async; / *以前称为tp_compare(Python 2)或tp_reserved(Python 3)* / reprfunc tp_repr; / *标准类的方法套件* / PyNumberMethods * tp_as_number; PySequenceMethods * tp_as_sequence; PyMappingMethods * tp_as_mapping; / *更多标准操作(此处为二进制兼容性)* / hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; / *访问对象作为输入/输出缓冲区的功能* / PyBufferProcs * tp_as_buffer; / *标志定义可选/扩展功能的存在* /无符号长tp_flags; const char * tp_doc; / *文档字符串* / / *在2.0版中分配的含义* / / *所有可访问对象的调用函数* / traverseproc tp_traverse; / *删除对包含对象的引用* /查询tp_clear; / *在2.1版中分配的含义* / / *丰富的比较* / richcmpfunc tp_richcompare; / *弱参考启用码* / Py_ssize_t tp_weaklistoffset; / *迭代器* / getiterfunc tp_iter; iternextfunc tp_iternext; / *属性描述符和子类化的东西* / struct PyMethodDef * tp_methods; struct PyMemberDef * tp_members; struct PyGetSetDef * tp_getset; struct _typeobject * tp_base; PyObject * tp_dict; descrgetfunc tp_descr_get; descrsetfunc tp_descr_set; Py_ssize_t tp_dictoffset; initproc tp_init; allocfunc tp_alloc; newfunc tp_new; freefunc tp_free; / *低级自由内存例程* /查询tp_is_gc; / *对于PyObject_IS_GC * / PyObject * tp_bases; PyObject * tp_mro; / *方法解析顺序* / PyObject * tp_cache; PyObject * tp_subclasses; PyObject * tp_weaklist;析构函数tp_del; / *类型属性高速缓存版本标记。在2.6版中添加* / unsigned int tp_version_tag;析构函数tp_finalize; vectorcallfunc tp_vectorcall; };

顺便说一句,请注意,类型的第一个成员不是PyObject而是PyVarObject,其定义如下:

typedef struct {PyObject ob_base; Py_ssize_t ob_size; / *可变部分中的项目数* /} PyVarObject;

但是,由于PyVarObject的第一个成员是PyObject,所以仍然可以将指向类型的指针转​​换为指向PyObject的指针。

那么,什么是类型?为什么它有这么多成员?类型确定该类型的对象的行为。类型的每个成员(称为插槽)负责对象行为的特定方面。例如:

tp_str是指向为该类型的对象实现str()的函数的指针。

tp_hash是指向为该类型的对象实现hash()的函数的指针。

一些插槽(称为子插槽)在套件中分组在一起。套件只是包含相关插槽的结构。例如,PySequenceMethods结构是实现序列协议的一组子插槽:

typedef struct {lenfunc sq_length; binaryfunc sq_concat; ssizeargfunc sq_repeat; ssizeargfunc sq_item;无效* was_sq_slice; ssizeobjargproc sq_ass_item;无效* was_sq_ass_slice; objobjproc sq_contains; binaryfunc sq_inplace_concat; ssizeargfunc sq_inplace_repeat; pySequenceMethods;

如果计算所有插槽和子插槽的数量,您会得到一个可怕的数字。幸运的是,每个插槽在《 Python / C API参考手册》中都有很好的记录(我强烈建议您为该链接添加书签)。今天,我们将只介绍几个插槽。但是,它应该使我们对如何使用插槽有一个总体了解。

由于我们对CPython如何添加对象感兴趣,因此我们找到负责添加的插槽。必须至少有一个这样的插槽。仔细检查PyTypeObject结构后,我们发现它具有" number"套件PyNumberMethods,该套件的第一个插槽是一个名为nd_add的二进制函数:

typedef struct {binaryfunc nb_add; // typedef PyObject *(* binaryfunc)(PyObject *,PyObject *)binaryfunc nb_subtract; binaryfunc nb_multiply; binaryfunc nb_remainder; binaryfunc nb_divmod; // ...更多子插槽} PyNumberMethods;

看来nb_add插槽正是我们要寻找的。关于此广告位自然会引起两个问题:

我认为最好从第二个开始。我们应该期望VM调用nb_add来执行BINARY_ADD操作码。因此,暂时让我们暂停对类型的讨论,并看看BINARY_ADD操作码是如何实现的。

案例TARGET(BINARY_ADD):{PyObject * right = POP(); PyObject *左= TOP(); PyObject *总和; / *注意(haypo):请不要尝试使用字节码在CPython上微优化int + int,这简直一文不值。有关讨论,请参见http://bugs.python.org/issue21955和http://bugs.python.org/issue10044。简而言之,没有补丁对现实的基准产生任何影响,只是微基准测试的微小提升。 * / if(PyUnicode_CheckExact(左)&& PyUnicode_CheckExact(右)){sum = unicode_concatenate(tstate,left,right,f,next_instr); / * unicode_concatenate使用了对左侧的引用* /} else {sum = PyNumber_Add(left,right); Py_DECREF(左); } Py_DECREF(右); SET_TOP(sum);如果(sum == NULL)转到错误; DISPATCH(); }

此代码需要一些注释。我们可以看到它调用PyNumber_Add()以添加两个对象,但是如果对象是字符串,它将改为调用unicode_concatenate()。为什么这样?这是一个优化。 Python字符串似乎是不可变的,但有时CPython会更改字符串,从而避免创建新的字符串。考虑将一个字符串附加到另一个字符串:

如果输出变量指向​​没有其他引用的字符串,则可以安全地对该字符串进行突变。这正是unicode_concatenate()实现的逻辑。

在评估循环中还要处理其他特殊情况并进行优化(例如整数和浮点数)可能很诱人。该评论明确警告不要这样做。问题在于,新的特殊情况带有附加检查,并且此检查仅在成功时才有用。否则,可能会对性能产生负面影响。

PyObject * PyNumber_Add(PyObject * v,PyObject * w){// NB_SLOT(nb_add)扩展为" offsetof(PyNumberMethods,nb_add)" PyObject *结果= binary_op1(v,w,NB_SLOT(nb_add));如果(结果== Py_NotImplemented){PySequenceMethods * m = Py_TYPE(v)-> tp_as_sequence; Py_DECREF(结果); if(m& m-> sq_concat){返回(* m-> sq_concat)(v,w); } result = binop_type_error(v,w," +");返回结果; }

我建议立即进入binary_op1()并弄清楚PyNumber_Add()的其余部分以后做什么:

静态PyObject * binary_op1(PyObject * v,PyObject * w,const int op_slot){PyObject * x; binaryfunc slotv = NULL; binaryfunc slotw = NULL; if(Py_TYPE(v)-> tp_as_number!= NULL)slotv = NB_BINOP(Py_TYPE(v)-> tp_as_number,op_slot);如果(!Py_IS_TYPE(w,Py_TYPE(v))&& Py_TYPE(w)-> tp_as_number!= NULL){slotw = NB_BINOP(Py_TYPE(w)-> tp_as_number,op_slot);如果(slotw == slotv)slotw = NULL; } if(slotv){if(slotw&& PyType_IsSubtype(Py_TYPE(w),Py_TYPE(v))){x = slotw(v,w);如果(x!= Py_NotImplemented)返回x; Py_DECREF(x); / *无法做到* / slotw = NULL; } x = slotv(v,w);如果(x!= Py_NotImplemented)返回x; Py_DECREF(x); / *无法做到* /}如果(slotw){x = slotw(v,w);如果(x!= Py_NotImplemented)返回x; Py_DECREF(x); / *无法做到* /} Py_RETURN_NOTIMPLEMENTED; }

binary_op1()函数采用三个参数:左操作数,右操作数和标识插槽的偏移量。两个操作数的类型都可以实现插槽。因此,binary_op1()查找两种实现。为了计算结果,它依赖以下逻辑调用一个实现或另一个实现:

如果一个操作数的类型是另一个操作数的子类型,则调用该子类型的插槽。

如果左侧操作数没有该插槽,请调用右侧操作数的插槽。

优先考虑子类型的插槽的原因是允许子类型覆盖其祖先的行为:

$ python -q>>> class HungryInt(int):... def __add__(self,o):... return self ...>>> x = HungryInt(5)>> x + 2 5>> 2 + x 7>> HungryInt。 __radd__ = lambda self,o:self>>> 2 + 5

让我们回到PyNumber_Add()。如果binary_op1()成功,则PyNumber_Add()仅返回binary_op1()的结果。但是,如果binary_op1()返回NotImplemented常量,则意味着不能对给定的类型组合执行该操作,则PyNumber_Add()调用sq_concat" sequence"。第一个操作数的插槽,并返回此调用的结果:

通过实现nb_add或sq_concat,类型可以支持+运算符。这些插槽具有不同的含义:

诸如int和float的内置类型实现nb_add,诸如str和list的内置类型实现sq_concat。从技术上讲,没有太大区别。选择一个插槽而不是另一个插槽的主要原因是要指出适当的含义。实际上,sq_concat插槽是不必要的,因此对于所有用户定义的类型(即类),都将其设置为NULL。

我们看到了nb_add插槽的用法:binary_op1()函数调用了该插槽。下一步是查看其设置。

由于加法对于不同类型是不同的操作,因此类型的nb_add插槽必须是以下两项之一:

它是一个与类型无关的函数,它调用某些特定于类型的函数,例如类型的__add __()特殊方法。

确实是这两者之一,而哪一个取决于类型。例如,诸如int和float之类的内置类型具有其自己的nb_add实现。相反,所有类共享相同的实现。从根本上讲,内置类型和类是一回事-PyTypeObject的实例。它们之间的重要区别是它们的创建方式。这种差异会影响插槽设置的方式,因此我们应该对其进行讨论。

静态定义类型的一个示例是任何内置类型。例如,下面是CPython如何定义浮点类型的方法:

PyTypeObject PyFloat_Type = {PyVarObject_HEAD_INIT(& PyType_Type,0)" float" ,sizeof(PyFloatObject),0,(析构函数)float_dealloc,/ * tp_dealloc * / 0,/ * tp_vectorcall_offset * / 0,/ * tp_getattr * / 0,/ * tp_setattr * / 0,/ * tp_as_async * /(reprfunc)float_repr ,/ * tp_repr * /& float_as_number,/ * tp_as_number * / 0,/ * tp_as_sequence * / 0,/ * tp_as_mapping * /(hashfunc)float_hash,/ * tp_hash * / 0,/ * tp_call * / 0,/ * tp_str * / PyObject_GenericGetAttr,/ * tp * / 0,/ * tp_setattro * / 0,/ * tp_as_buffer * / Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/ * tp_flags * / float_new__doc__,/ * tp_doc * / 0,/ * tp_traverse * / 0,/ * tp_clear * / float_richcompare,/ * tp_richcompare * / 0,/ * tp_weaklistoffset * / 0,/ * tp_iter * / ,/ * tp_iternext * / float_methods,/ * tp_methods * / 0,/ * tp_members * / float_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 * / float_new,/ * tp_new * /};

静态定义类型的插槽已明确指定。通过查看" number&#34 ;,我们可以轻松地看到float类型如何实现nb_add。套房:

静态PyNumberMethods float_as_number = {float_add,/ * nb_add * / float_sub,/ * nb_subtract * / float_mul,/ * nb_multiply * / // ... ...更多数字插槽};

静态PyObject * float_add(PyObject * v,PyObject * w){double a,b; CONVERT_TO_DOUBLE(v,a); CONVERT_TO_DOUBLE(w,b); a = a + b;返回PyFloat_FromDouble(a); }

对于我们的讨论,浮点算法并不那么重要。本示例演示如何指定静态定义的类型的行为。事实证明这很容易:只需编写插槽的实现并将每个插槽指向相应的实现即可。

如果您想学习如何静态定义自己的类型,请查看C / C ++程序员的Python教程。

动态分配的类型是我们使用class语句定义的类型。正如我们已经说过的那样,它们是PyTypeObject的实例,就像静态定义的类型一样。传统上,我们称它们为类,但也可以称它们为用户定义类型。

从程序员的角度来看,在Python中定义类比在C中定义类型要容易。这是因为CPython创建类时在幕后做了很多事情。让我们看看此过程涉及什么。

如果有时间,请随时进行操作,或者阅读Eli Bendersky上关于班级的文章。我们会走捷径。

通过调用类型(例如)创建对象。 list()或MyClass()。通过调用元类型创建类。元类型只是实例是类型的类型。 Python具有一个称为PyType_Type的内置元类型,我们将其简称为type。定义方式如下:

PyTypeObject PyType_Type = {PyVarObject_HEAD_INIT(& PyType_Type,0)" type" ,/ * tp_name * / sizeof(PyHeapTypeObject),/ * tp_basicsize * / sizeof(PyMemberDef),/ * tp_itemsize * /(destructor)type_dealloc,/ * tp_dealloc * / offsetof(PyTypeObject,tp_vectorcall),/ * tp_vectorcall / * tp_getattr * / 0,/ * tp_setattr * / 0,/ * tp_as_async * /(reprfunc)type_repr,/ * tp_repr * / 0,/ * tp_as_number * / 0,/ * tp_as_sequence * / 0,/ * tp_as_mapping * / 0,/ * tp_hash * /(ternaryfunc)type_call,/ * tp_call * / 0,/ * tp_str * /(getattrofunc)type_getattro,/ * tp_getattro * /(setattrofunc)type_setattro,/ * tp_setattro * / 0,/ * tp_buffer / Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_VECTORCALL,/ * tp_flags * / type_doc,/ * tp_doc * /(traverseproc)type_traverse,/ * tp_traverse * /(查询)type_clear,/ * tp_clear * / 0,/ * tp_richcompare * / offsetof_(PyTypelist,t * ak) tp_weaklistoffset * / 0,/ * tp_iter * / 0,/ * tp_iternext * / type_methods,/ * tp_methods * / type_members,/ * tp_members * / type_getsets,/ * tp_getset * / 0,/ * tp_base * / 0,/ * tp_dict * / 0,/ * tp_descr_get * / 0,/ * tp_descr_set * / offsetof(PyTypeObject,tp_dict),/ * tp_dictoffset * / type_init,/ * tp_init * / 0,/ * tp_alloc * / type_new,/ * tp_new * / PyObject_GC_Del ,/ * tp_free * /(查询)type_is_gc,/ * tp_is_gc * /};

所有内置类型的类型均为type,所有类的类型默认为type。因此,类型决定了类型的行为。例如,当我们调用诸如list()或MyClass()之类的类型时,将通过类型的tp_call插槽指定发生什么。 tp_ca的执行

......