Python属性如何工作

2020-12-31 21:20:59

当我们获取或设置Python对象的属性时会发生什么?这个问题并不像乍看起来那样简单。的确,任何有经验的Python程序员都对属性如何工作有很好的直观理解,而文档则可以极大地增进理解。但是,当出现有关属性的真正平凡的问题时,直觉将失败,文档也将无济于事。为了获得深刻的理解并能够回答此类问题,必须研究如何实现属性。这就是我们今天要做的。

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

上次我们研究了Python对象系统如何工作。我们在那部分中学到的一些知识对于我们当前的讨论至关重要,因此让我们简要回顾一下。

Python对象是具有至少两个成员的C结构的实例:

每个对象都必须具有类型,因为类型决定了对象的行为方式。类型也是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; };

一种类型的成员称为插槽。每个插槽负责对象行为的特定方面。例如,类型的tp_call插槽指定当我们调用该类型的对象时发生的情况。一些插槽在套件中组合在一起。套件的一个示例是" number"套件tp_as_number。上次我们研究了其nb_add插槽,该插槽指定如何添加对象。这些和所有其他插槽在文档中都有很好的描述。

如何设置类型的插槽取决于类型的定义。有两种在CPython中定义类型的方法:

静态定义的类型只是PyTypeObject的静态初始化实例。所有内置类型都是静态定义的。例如,以下是float类型的定义:

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 * /};

为了动态分配新类型,我们称为元类型。元类型是其实例为类型的类型。它确定类型的行为。特别是,它将创建新的类型实例。 Python具有一个称为类型的内置元类型。它是所有内置类型的元类型。它也用作创建类的默认元类型。 CPython执行class语句时,通常会调用type()创建类。我们也可以通过直接调用type()来创建一个类:

类型的tp_new插槽被调用以创建一个类。该插槽的实现是type_new()函数。此函数分配类型对象并进行设置。

静态定义类型的插槽是明确指定的。类的插槽由元类型自动设置。静态和动态定义的类型都可以从其基础继承一些插槽。

一些插槽被映射到特殊方法。如果一个类定义了对应于某个插槽的特殊方法,则CPython会自动将插槽设置​​为调用该特殊方法的默认实现。这就是为什么我们可以添加其类定义__add __()的对象的原因。 CPython对静态定义的类型进行相反的处理。如果这种类型实现了对应于某些特殊方法的插槽,则CPython会将特殊方法设置为包装该插槽的实现。这就是int类型如何获得其__add __()特殊方法的方式。

所有类型都必须通过调用PyType_Ready()函数进行初始化。这个功能可以做很多事情。例如,它执行插槽继承,并基于插槽添加特殊方法。对于一个类,PyType_Ready()由type_new()调用。对于静态定义的类型,必须显式调用PyType_Ready()。 CPython启动时,它将为每个内置类型调用PyType_Ready()。

什么是属性?我们可以说属性是与对象相关联的变量,但不仅限于此。很难给出一个捕获属性所有重要方面的定义。因此,让我们从确定的内容开始,而不是从定义开始。

像对象行为的任何其他方面一样,这些操作的作用取决于对象的类型。一个类型具有负责获取,设置和删除属性的某些插槽。 VM调用这些插槽以执行诸如value = obj.attr和obj.attr = value之类的语句。要查看VM如何执行操作以及这些插槽是什么,让我们应用熟悉的方法:

首先,我们了解获得属性值时VM的功能。编译器生成LOAD_ATTR操作码以加载值:

$ echo' obj.attr' | python -m dis 1 0 LOAD_NAME 0(obj)2 LOAD_ATTR 1(attr)...

案例TARGET(LOAD_ATTR):{PyObject *名称= GETITEM(名称,oparg); PyObject *所有者= TOP(); PyObject * res = PyObject_GetAttr(owner,name);复制代码Py_DECREF(owner); SET_TOP(res);如果(res == NULL)转到错误; DISPATCH(); }

我们可以看到VM调用了PyObject_GetAttr()函数来完成这项工作。这是此功能的作用:

PyObject * PyObject_GetAttr(PyObject * v,PyObject * name){PyTypeObject * tp = Py_TYPE(v);如果(!返回NULL; } if(tp-> tp_getattro!= NULL)返回(* tp-> tp_getattro)(v,name); if(tp-> tp_getattr!= NULL){const char * name_str = PyUnicode_AsUTF8(name);如果(name_str == NULL)返回NULL; return(* tp-> tp_getattr)(v,(char *)name_str); } PyErr_Format(PyExc_AttributeError,"'%。50s'对象没有属性'%U'",tp-> tp_name,name);返回NULL; }

它首先尝试调用该对象类型的tp_getattro插槽。如果未实现此插槽,它将尝试调用tp_getattr插槽。如果tp_getattr也未实现,则会引发AttributeError。

一种类型实现tp_getattro或tp_getattr或两者都支持属性访问。根据文档,它们之间的唯一区别是tp_getattro采用Python字符串作为属性名称,而tp_getattr采用C字符串。尽管存在选择,但是您不会在CPython中找到实现tp_getattr的类型,因为不推荐使用tp_getattro。

从VM的角度来看,设置属性与获取属性没有太大不同。编译器生成STORE_ATTR操作码以将属性设置为某个值:

$ echo' obj.attr = value' | python -m dis 1 0 LOAD_NAME 0(值)2 LOAD_NAME 1(obj)4 STORE_ATTR 2(attr)...

案例TARGET(STORE_ATTR):{PyObject *名称= GETITEM(名称,oparg); PyObject *所有者= TOP(); PyObject * v =秒();内部错误; STACK_SHRINK(2); err = PyObject_SetAttr(owner,name,v); Py_DECREF(v); Py_DECREF(owner); if(err!= 0)转到错误; DISPATCH(); }

int PyObject_SetAttr(PyObject * v,PyObject *名称,PyObject *值){PyTypeObject * tp = Py_TYPE(v);内部错误;如果(!PyUnicode_Check(name)){PyErr_Format(PyExc_TypeError,"属性名称必须是字符串,而不是'%。200s'",Py_TYPE(name)-> tp_name);返回-1; } Py_INCREF(name); PyUnicode_InternInPlace(& name); if(tp-> tp_setattro!= NULL){err =(* tp-> tp_setattro)(v,name,value); Py_DECREF(name);返回错误; } if(tp-> tp_setattr!= NULL){const char * name_str = PyUnicode_AsUTF8(name);如果(name_str == NULL){Py_DECREF(name);返回-1; } err =(* tp-> tp_setattr)(v,(char *)name_str,value); Py_DECREF(name);返回错误; } Py_DECREF(name); _PyObject_ASSERT(name,Py_REFCNT(name)> = 1); if(tp-> tp_getattr == NULL&& tp-> tp_getattro == NULL)PyErr_Format(PyExc_TypeError,"'%。100s'对象没有属性"& #34;(%s。%U)",tp-> tp_name,value == NULL?" del":" assign to",name); else PyErr_Format(PyExc_TypeError,"'%。100s'对象只有只读属性""(%s。%U)",tp-> tp_name,value == NULL?" del":"分配给",name);返回-1; }

此函数调用tp_setattro和tp_setattr插槽的方式与PyObject_GetAttr()调用tp_getattro和tp_getattr的方式相同。 tp_setattro插槽与tp_getattro配对,而tp_setattr与tp_getattr配对。就像tp_getattr一样,tp_setattr也已弃用。

请注意,PyObject_SetAttr()检查类型是否定义了tp_getattro或tp_getattr。类型必须实现属性访问以支持属性分配。

有趣的是,类型没有用于删除属性的特殊插槽。然后,什么指定如何删除属性?让我们来看看。编译器生成DELETE_ATTR操作码以删除属性:

$ echo' del obj.attr' | python -m dis 1 0 LOAD_NAME 0(obj)2 DELETE_ATTR 1(attr)

案例TARGET(DELETE_ATTR):{PyObject *名称= GETITEM(名称,oparg); PyObject *所有者= POP();内部错误; err = PyObject_SetAttr(owner,name,(PyObject *)NULL); Py_DECREF(owner); if(err!= 0)转到错误; DISPATCH(); }

要删除属性,VM调用与设置属性相同的PyObject_SetAttr()函数,因此,同一tp_setattro插槽负责删除属性。但是,它如何知道要执行两个操作中的哪一个呢? NULL值指示应删除该属性。

如本节所示,tp_getattro和tp_setattro插槽确定对象属性的工作方式。接下来想到的一个问题是:这些插槽如何实现?

适当签名的任何功能都可以是tp_getattro和tp_setattro的实现。类型可以以绝对任意的方式实现这些插槽。幸运的是,我们仅需研究一些实现即可了解Python属性的工作方式。这是因为大多数类型使用相同的通用实现。

用于获取和设置属性的通用函数是PyObject_GenericGetAttr()和PyObject_GenericSetAttr()。默认情况下,所有类都使用它们。大多数内置类型将它们明确指定为插槽实现,或者从也使用通用实现的对象继承它们。

在本文中,我们将重点介绍通用实现,因为它基本上就是我们所说的Python属性。我们还将讨论两种不使用通用实现的重要情况。第一种情况是类型。它以自己的方式实现tp_getattro和tp_setattro插槽,尽管其实现与通用的实现非常相似。第二种情况是通过定义__getattribute __(),__getattr __(),__setattr __()和__delattr __()特殊方法自定义属性访问和分配的任何类。 CPython将此类的tp_getattro和tp_setattro插槽设置为调用这些方法的函数。

PyObject_GenericGetAttr()和PyObject_GenericSetAttr()函数实现了我们都习惯的属性的行为。当我们将对象的属性设置为某个值时,CPython将该值放入对象的字典中:

$ python -q>>> A级:...通过...>> a = A()>>一种 。 __dict__ {}>>>一种 。 x ='实例属性' >>>一种 。 __dict__ {' x&#39 ;:'实例属性'}

当我们尝试获取属性的值时,CPython从对象的字典中加载它:

如果对象的字典不包含属性,则CPython从类型的字典中加载值:

如果类型的字典也不包含该属性,则CPython会在类型父母的字典中搜索值:

实例变量存储在对象字典中,类型变量存储在类型字典中以及类型父母的字典中。要将属性设置为某个值,CPython只需更新对象的字典即可。为了获取属性的值,CPython首先在对象的字典中搜索该属性,然后在类型的字典中以及该类型的父母的字典中进行搜索。 CPython在搜索值时对类型进行迭代的顺序是“方法解析顺序”(MRO)。

从技术上讲,描述符是一个Python对象,其类型实现某些插槽:tp_descr_get或tp_descr_set或两者。本质上,描述符是一个Python对象,当用作属性时,它控制着我们获取,设置或删除它的情况。如果PyObject_GenericGetAttr()发现属性值是其类型实现tp_descr_get的描述符,则它不像通常那样返回值,而是调用tp_descr_get并返回此调用的结果。 tp_descr_get插槽具有三个参数:描述符本身,正在查找其属性的对象以及对象的类型。由tp_descr_get决定如何处理参数以及返回什么。同样,PyObject_GenericSetAttr()查找当前属性值。如果发现该值是其类型实现tp_descr_set的描述符,则它将调用tp_descr_set而不是仅更新对象的字典。传递给tp_descr_set的参数是描述符,对象和新属性值。要删除属性,PyObject_GenericSetAttr()调用tp_descr_set并将新属性值设置为NULL。

一方面,描述符使Python属性有些复杂。另一方面,描述符使Python属性变得强大。正如Python的词汇表所述,

理解描述符是深入了解Python的关键,因为它们是许多功能(包括函数,方法,属性,类方法,静态方法以及对超类的引用)的基础。

让我们修改上一部分中讨论的描述符的一个重要用例:方法。

类型字典中的函数的工作方式不同于普通函数,而是方法。也就是说,我们在调用第一个参数时无需显式传递它:

但是,如果我们查找&f'的值,在类型字典中,我们将获得原始功能:

CPython不返回存储在字典中的值,而是返回其他值。这是因为函数是描述符。该函数类型实现tp_descr_get插槽,因此PyObject_GenericGetAttr()调用此插槽并返回调用结果。调用的结果是一个既存储函数又存储实例的方法对象。当我们调用一个方法对象时,实例被放在参数列表的前面,并且该函数被调用。

描述符仅在用作类型变量时才具有其特殊的行为。当它们用作实例变量时,它们的行为类似于普通对象。例如,放在对象字典中的函数不会成为方法:

显然,语言设计师没有发现将描述符用作实例变量的情况是一个好主意。该决定的一个很好的结果是实例变量非常简单。它们只是数据。

函数类型是内置描述符类型的示例。我们还可以定义自己的描述符。为此,我们创建一个实现描述符协议的类:__get __(),__set __()和__delete __()特殊方法:

>>> class DescrClass:... def __get__(self,obj,type = None):...打印('我可以做任何事')...返回self ...>>一种 。 descr_attr = DescrClass()>>一种 。 descr_attr我可以做任何事情< __ main __。DescrClass object at 0x108b458e0>

如果类定义了__get __(),则CPython将其tp_descr_get插槽设置为调用该方法的函数。如果一个类定义了__set __()或__delete __(),则CPython将其tp_descr_set插槽设置为在值为NULL时调用__delete __()的函数,否则调用__set __()。

如果您想知道为什么有人会在第一个p中定义我们的描述符 ......