# Python 的属性是如何工作的?
当我们获取或设置 Python 对象的属性时会发生什么?为了对更好理解属性是如何工作的,我们来研究一下属性是如何实现的。
本篇使用的 CPython 的 3.9.12 版本,可能与前面的版本会有变化
Python 对象是至少有两个成员的 C 结构体的实例:
- 引用计数(a reference count)
- 指向对象类型的指针(a pointer to the object’s type)
每个对象都必须有一个类型,因为该类型将决定对象的行为,当然,类型也是一个 Python 对象,也就是 PyTypeObject 结构的一个实例
// include/cpython/object.h | |
// PyTypeObject is a typedef for "struct _typeobject" | |
struct _typeobject { | |
PyObject_VAR_HEAD | |
const char *tp_name; /* For printing, in format "<module>.<name>" */ | |
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ | |
/* Methods to implement standard operations */ | |
destructor tp_dealloc; | |
Py_ssize_t tp_vectorcall_offset; | |
getattrfunc tp_getattr; | |
setattrfunc tp_setattr; | |
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2) | |
or tp_reserved (Python 3) */ | |
reprfunc tp_repr; | |
/* Method suites for standard classes */ | |
PyNumberMethods *tp_as_number; | |
PySequenceMethods *tp_as_sequence; | |
PyMappingMethods *tp_as_mapping; | |
/* More standard operations (here for binary compatibility) */ | |
hashfunc tp_hash; | |
ternaryfunc tp_call; | |
reprfunc tp_str; | |
getattrofunc tp_getattro; | |
setattrofunc tp_setattro; | |
/* Functions to access object as input/output buffer */ | |
PyBufferProcs *tp_as_buffer; | |
/* Flags to define presence of optional/expanded features */ | |
unsigned long tp_flags; | |
const char *tp_doc; /* Documentation string */ | |
/* Assigned meaning in release 2.0 */ | |
/* call function for all accessible objects */ | |
traverseproc tp_traverse; | |
/* delete references to contained objects */ | |
inquiry tp_clear; | |
/* Assigned meaning in release 2.1 */ | |
/* rich comparisons */ | |
richcmpfunc tp_richcompare; | |
/* weak reference enabler */ | |
Py_ssize_t tp_weaklistoffset; | |
/* Iterators */ | |
getiterfunc tp_iter; | |
iternextfunc tp_iternext; | |
/* Attribute descriptor and subclassing stuff */ | |
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; /* Low-level free-memory routine */ | |
inquiry tp_is_gc; /* For PyObject_IS_GC */ | |
PyObject *tp_bases; | |
PyObject *tp_mro; /* method resolution order */ | |
PyObject *tp_cache; | |
PyObject *tp_subclasses; | |
PyObject *tp_weaklist; | |
destructor tp_del; | |
/* Type attribute cache version tag. Added in version 2.6 */ | |
unsigned int tp_version_tag; | |
destructor tp_finalize; | |
vectorcallfunc tp_vectorcall; | |
}; |
类型的成员称为槽,每个槽负责对象的特定行为,例如类型的 tp_call,将会指定我们调用该类型的对象时会发生什么。
如何设置类型的槽取决于如何定义类型,在 CPython 中定义类型有两种方法:
- 静态(statically)
- 动态(dynamically)
静态定义的类型只是 PyTypeObject 的静态初始化实例,所有内置类型都是静态定义的,比如 float 类型的定义
PyTypeObject PyFloat_Type = { | |
PyVarObject_HEAD_INIT(&PyType_Type, 0) | |
"float", | |
sizeof(PyFloatObject), | |
0, | |
(destructor)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_getattro */ | |
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 */ | |
0, /* 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 */ | |
}; |
如果要动态分配新类型,我们可以调用元类型,元类型是实例为 type 类型的类型。Python 有一个 type 的内置类,它是所有类型的原型,它被用作创建类的默认元类型。当 CPython 执行 class 语句时,它通常会调用 type () 来创建类。我们也可以通过直接调用 type () 来动态创建一个类
MyClass = type(name, bases, namespace) |
类型的 tp_new 被调用以创建一个类,也就是__new__方法,该函数分配类型对象和空间并设置它。
所有类型都必须通过调用 PyType_Ready () 函数来初始化,PyType_Ready () 由 type_new () 调用。对于静态定义的类型,必须显式调用 PyType_Ready ()。当 CPython 启动时,它为每个内置类型调用 PyType_Ready ()。
# Attributes and the VM
在 Python 中,可以使用属性做三件事:
- 获取属性的值:value = obj.attr
- 设置属性值: obj.attr = value
- 删除属性: del obj.attr
与对象行为的其它一样,这些操作的作用取决于对象的类型。一个类型有一些负责获取、设置和删除属性的槽,VM 通过调用这些槽来执行 value = obj.attr 和 obj.attr = value 等语句。
那么 VM 是如果做到的,我们通过一下方法观察:
- 编写一段获取 / 设置 / 删除属性的代码
- 使用内置 dis module,反汇编为 bytecode
- 生成的 bytecode 指令的实现 ceval.c
# Getting an attribute
当我们得到一个属性的值时,VM 会做什么,通过字节码观察到,编译器生成 LOAD_ATTR 操作码来加载值
>>> dis.dis('obj.attr') | |
1 0 LOAD_NAME 0 (obj) | |
2 LOAD_ATTR 1 (attr) | |
4 RETURN_VALUE |
VM 执行这个操作码
case TARGET(LOAD_ATTR): { | |
PyObject *name = GETITEM(names, oparg); | |
PyObject *owner = TOP(); | |
PyObject *res = PyObject_GetAttr(owner, name); | |
Py_DECREF(owner); | |
SET_TOP(res); | |
if (res == NULL) | |
goto error; | |
DISPATCH(); | |
} |
可以看到 VM 调用了 PyObject_GetAttr 函数来完成工作
PyObject * | |
PyObject_GetAttr(PyObject *v, PyObject *name) | |
{ | |
PyTypeObject *tp = Py_TYPE(v); | |
if (!PyUnicode_Check(name)) { | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return NULL; | |
} | |
if (tp->tp_getattro != NULL) | |
return (*tp->tp_getattro)(v, name); | |
if (tp->tp_getattr != NULL) { | |
const char *name_str = PyUnicode_AsUTF8(name); | |
if (name_str == NULL) | |
return NULL; | |
return (*tp->tp_getattr)(v, (char *)name_str); | |
} | |
PyErr_Format(PyExc_AttributeError, | |
"'%.50s' object has no attribute '%U'", | |
tp->tp_name, name); | |
return NULL; | |
} |
它首先尝试调用 tp_getattro,如果没有实现该方法,它会再尝试调用 tp_getattr,如果也没有实现,则会引发 AttributeError。
type 实现 tp_getattro 或 tp_getattr 或者两者,都支持属性访问,唯一区别在于 tp_getattro 采用 Python 字符串作为属性的名称,而 tp_getattr 采用 C 字符串,不过 tp_getattr 已经被弃用了,转而使用 tp_getattro。
# Setting an attribute
从 VM 的角度来说,设置属性与获取属性没太大区别,编译器生成 STORE_ATTR 操作码,将属性设置为某个值
>>> dis.dis('obj.attr = value') | |
1 0 LOAD_NAME 0 (value) | |
2 LOAD_NAME 1 (obj) | |
4 STORE_ATTR 2 (attr) | |
6 LOAD_CONST 0 (None) | |
8 RETURN_VALUE |
STORE_ATTR 操作码
case TARGET(STORE_ATTR): { | |
PyObject *name = GETITEM(names, oparg); | |
PyObject *owner = TOP(); | |
PyObject *v = SECOND(); | |
int err; | |
STACK_SHRINK(2); | |
err = PyObject_SetAttr(owner, name, v); | |
Py_DECREF(v); | |
Py_DECREF(owner); | |
if (err != 0) | |
goto error; | |
DISPATCH(); | |
} |
可以发现,是通过 PyObject_SetAttr 来完成设置工作
int | |
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) | |
{ | |
PyTypeObject *tp = Py_TYPE(v); | |
int err; | |
if (!PyUnicode_Check(name)) { | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return -1; | |
} | |
Py_INCREF(name); | |
PyUnicode_InternInPlace(&name); | |
if (tp->tp_setattro != NULL) { | |
err = (*tp->tp_setattro)(v, name, value); | |
Py_DECREF(name); | |
return err; | |
} | |
if (tp->tp_setattr != NULL) { | |
const char *name_str = PyUnicode_AsUTF8(name); | |
if (name_str == NULL) { | |
Py_DECREF(name); | |
return -1; | |
} | |
err = (*tp->tp_setattr)(v, (char *)name_str, value); | |
Py_DECREF(name); | |
return err; | |
} | |
Py_DECREF(name); | |
_PyObject_ASSERT(name, Py_REFCNT(name) >= 1); | |
if (tp->tp_getattr == NULL && tp->tp_getattro == NULL) | |
PyErr_Format(PyExc_TypeError, | |
"'%.100s' object has no attributes " | |
"(%s .%U)", | |
tp->tp_name, | |
value==NULL ? "del" : "assign to", | |
name); | |
else | |
PyErr_Format(PyExc_TypeError, | |
"'%.100s' object has only read-only attributes " | |
"(%s .%U)", | |
tp->tp_name, | |
value==NULL ? "del" : "assign to", | |
name); | |
return -1; | |
} |
# Deleting an attribute
在 Python 中,有趣的是,类型没有用于删除属性的特殊槽。那么要如何指定删除属性?通过编译器生成 DELETE_ATTR 操作码以删除一个属性
>>> dis.dis('del obj.attr') | |
1 0 LOAD_NAME 0 (obj) | |
2 DELETE_ATTR 1 (attr) | |
4 LOAD_CONST 0 (None) | |
6 RETURN_VALUE |
VM 通过执行这个操作码来实现
case TARGET(DELETE_ATTR): { | |
PyObject *name = GETITEM(names, oparg); | |
PyObject *owner = POP(); | |
int err; | |
err = PyObject_SetAttr(owner, name, (PyObject *)NULL); | |
Py_DECREF(owner); | |
if (err != 0) | |
goto error; | |
DISPATCH(); | |
} |
删除属性时,VM 调用相同的 PyObject_SetAttr 函数来设置属性,tp_setattro 槽负责删除属性,但它通过 NULL 来指示删除该属性。
# Generic attribute management
PyObject_GenericGetAttr 和 PyObject_GenericSetAttr 函数实现了属性行为,比如将一个对象的属性设置为某个值时,CPython 会将该值放入对象的 dictionary 中
>>> class A: | |
... pass | |
... | |
>>> a = A() | |
>>> a.__dict__ | |
{} | |
>>> a.x = "x attr" | |
>>> a.__dict__ | |
{'x': 'x attr'} |
当我们尝试获取属性的值时,CPython 就会从对象的 dictionary 中加载它
>>> a.x | |
'x attr' |
如果对象的 dictionary 中不存在该属性,CPython 将从该类型的 dictionary 中加载它
>>> A.y = "class y attr" | |
>>> a.y | |
'class y attr' |
如果该类型的 dictionary 也不存在该属性,CPython 将会在该类型父类的 dictionary 中搜索该值
>>> class B(A): | |
... pass | |
... | |
>>> b = B() | |
>>> b.y | |
'class y attr |
一个对象的属性有两种情况:
- 实例变量
- 类型变量
实例变量存储在对象的字典中,类型变量存储在类型的字典和类型父类的字典中。要将属性设置为某个值,CPython 只需要更新对象的 dictionary。为了获取属性的值,CPython 首先会在对象的 dictionary 中查找它,如果没有找到,就会在类型的 dictionary 和类型父类的 dictionary 中查找它。CPython 在查找值时会迭代这些类型 MRO(Method ResoltionOrder)。
# Descriptors
什么是描述符,描述符是一个 Python 对象,其类型实现了 tp_descr_get 或 tp_descr_set 方法,也就是 Python 中的__get__和__set__方法。如果 PyObject_GerericGetAttr 函数在调用中发现属性值是一个描述符,其类型实现了 tp_descr_get 方法,那么就会调用 tp_descr_get 方法并返回调用结果。tp_descr_get 方法接受三个参数:描述符本身,正在查找其属性的对象和对象的类型。至于返回什么取决于 tp_descr_get 的实现。
Python 中的 property 就是基于 tp_descr_get 和 tp_descr_set 实现的:Property 源码分析
PyObject_GenericSetAttr 函数在查找当前属性值时,如果发现该值的类型实现了 tp_descr_set 方法,就会调用 tp_descr_set 方法,而不仅仅是更新对象的字典。传递给 tp_descr_set 的参数有描述符本身、对象和新属性值。要删除一个属性,PyObject_GenericSetAttr 也会调用 tp_descr_set,但将新属性值设置为 NULL。
Understanding descriptors is a key to a deep understanding of Python because they are the basis for many features including functions, methods, properties, class methods, static methods, and reference to super classes.
在类型中的方法不同于普通函数,比如说当我们调用一个对象的方法时,我们不需要显式的传递它
>>> A.f = lambda self: self | |
>>> a.f() | |
<__main__.A object at 0x00000128F0AEB490> |
A.f 看起来像属性,它还是一个方法。
>>> a.f | |
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>> |
如果我们直接通过类型的字典中查找 f 值,我们将得到原来的函数
>>> A.__dict__['f'] | |
<function <lambda> at 0x00000128F067F160> |
CPython 返回的不是字典中存储的值,而是其他值,因为函数是描述符,其函数类型实现了 tp_descr_get 方法,所以,当你调用 PyObject_GenericGetAttr 时,它就会调用 tp_descr_get 方法并返回调用结果。
当我们调用一个方法对象时,实例会被放在参数列表的前面,然后函数调用
/* Bind a function to an object */ | |
static PyObject * | |
func_descr_get(PyObject *func, PyObject *obj, PyObject *type) | |
{ | |
if (obj == Py_None || obj == NULL) { | |
Py_INCREF(func); | |
return func; | |
} | |
return PyMethod_New(func, obj); // 变成了 method 对象 | |
} | |
// 最终调用该方法 | |
static PyObject * | |
method_vectorcall(PyObject *method, PyObject *const *args, | |
size_t nargsf, PyObject *kwnames) | |
{ | |
PyObject *self = PyMethod_GET_SELF(method); // 获取 self | |
...... | |
newargs[0] = self; | |
* undefined behaviour. */ | |
...... | |
// 将 self 作为第一个参数 | |
result = _PyObject_VectorcallTstate(tstate, func, | |
newargs, nargs+1, kwnames); | |
...... | |
return result; | |
} |
需要注意的是,描述符只有在类型变量时才会触发,当被作为实例变量时,它的行为和普通对象相似,放在对象字典中的函数不会成为方法。
>>> a.f | |
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>> | |
>>> a.f() | |
<__main__.A object at 0x00000128F0AEB490> | |
>>> a.g | |
<function <lambda> at 0x00000128F0B4E670> | |
>>> a.g() | |
Traceback (most recent call last): | |
File "<stdin>", line 1, in <module> | |
TypeError: <lambda>() missing 1 required positional argument: 'self' |
我们可以自定义一个描述符类型,实现__get__、__set__、__delete__魔法方法
>>> class DescrClass: | |
... def __get__(self, obj, type=None): | |
... print("something happed...") | |
... return self | |
... | |
>>> A.descr_attr = DescrClass() | |
>>> A.descr_attr | |
something happed... | |
<__main__.DescrClass object at 0x00000128F0536CD0> |
如果一个类定义了__get__,CPython 将其 tp_descr_get 设置为调用该方法的函数。如果一个类定义了__set__或者__delete__,CPython 在 tp_descr_set 设置为当前值为 NULL 时会调用__delete__函数,否则调用__set__函数。
If you wonder why anyone would want to define their our descriptors in the first place, check out the excellent Descriptor HowTo Guide by Raymond Hettinger.
# Object’s dictionary and type’s dictionary
对象的字典是存储实例变量的字典。类型的每个对象都有一个指向自己字典的指针。例如函数对象的 func_dict。
typedef struct { | |
// ...... | |
PyObject *func_dict; /* The __dict__ attribute, | |
// ...... | |
} PyFunctionObject; |
要告诉 CPython 某个对象的哪个成员是指向该对象的字典的指针,该对象的类型使用 tp_dictoffset 来指定该成员的偏移量。
PyTypeObject PyFunction_Type = { | |
// ...... | |
offsetof(PyFunctionObject, func_dict), // tp_dictoffset | |
// ...... | |
}; |
tp_dictoffset 的正值指定从对象的结构开始的偏移量。负值指定从结构的末尾开始的偏移量。零偏移量表示该类型的对象没有字典。例如整数对象
>>> (12).__dict__ | |
Traceback (most recent call last): | |
File "<stdin>", line 1, in <module> | |
AttributeError: 'int' object has no attribute '__dict__' |
通过__dictoffset__属性可以看到 int 类型的 tp_dictoffset 设置为 0 了
>>> int.__dictoffset__ | |
0 |
当我们知道描述符是什么以及属性存储在哪里,就可以看到 PyObject_GenericGetAttr 和 PyObject_GenericSetAttr 函数的作用了
# PyObject_GenericSetAttr
PyObject_GenericSetAttr 将属性设置为特定的值,但实际上真正实现的是另一个函数
int | |
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value) | |
{ | |
return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL); | |
} | |
PyObject * | |
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, | |
PyObject *dict, int suppress) | |
{ | |
/* Make sure the logic of _PyObject_GetMethod is in sync with | |
this method. | |
When suppress=1, this function suppress AttributeError. | |
*/ | |
PyTypeObject *tp = Py_TYPE(obj); | |
PyObject *descr = NULL; | |
PyObject *res = NULL; | |
descrgetfunc f; | |
Py_ssize_t dictoffset; | |
PyObject **dictptr; | |
if (!PyUnicode_Check(name)){ | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return NULL; | |
} | |
Py_INCREF(name); | |
if (tp->tp_dict == NULL) { | |
if (PyType_Ready(tp) < 0) | |
goto done; | |
} | |
descr = _PyType_Lookup(tp, name); | |
f = NULL; | |
if (descr != NULL) { | |
Py_INCREF(descr); | |
f = Py_TYPE(descr)->tp_descr_get; | |
if (f != NULL && PyDescr_IsData(descr)) { | |
res = f(descr, obj, (PyObject *)Py_TYPE(obj)); | |
if (res == NULL && suppress && | |
PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
goto done; | |
} | |
} | |
if (dict == NULL) { | |
/* Inline _PyObject_GetDictPtr */ | |
dictoffset = tp->tp_dictoffset; | |
if (dictoffset != 0) { | |
if (dictoffset < 0) { | |
Py_ssize_t tsize = Py_SIZE(obj); | |
if (tsize < 0) { | |
tsize = -tsize; | |
} | |
size_t size = _PyObject_VAR_SIZE(tp, tsize); | |
_PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX); | |
dictoffset += (Py_ssize_t)size; | |
_PyObject_ASSERT(obj, dictoffset > 0); | |
_PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0); | |
} | |
dictptr = (PyObject **) ((char *)obj + dictoffset); | |
dict = *dictptr; | |
} | |
} | |
if (dict != NULL) { | |
Py_INCREF(dict); | |
res = PyDict_GetItemWithError(dict, name); | |
if (res != NULL) { | |
Py_INCREF(res); | |
Py_DECREF(dict); | |
goto done; | |
} | |
else { | |
Py_DECREF(dict); | |
if (PyErr_Occurred()) { | |
if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
else { | |
goto done; | |
} | |
} | |
} | |
} | |
if (f != NULL) { | |
res = f(descr, obj, (PyObject *)Py_TYPE(obj)); | |
if (res == NULL && suppress && | |
PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
goto done; | |
} | |
if (descr != NULL) { | |
res = descr; | |
descr = NULL; | |
goto done; | |
} | |
if (!suppress) { | |
PyErr_Format(PyExc_AttributeError, | |
"'%.50s' object has no attribute '%U'", | |
tp->tp_name, name); | |
} | |
done: | |
Py_XDECREF(descr); | |
Py_DECREF(name); | |
return res; | |
} | |
PyObject * | |
PyObject_GenericGetAttr(PyObject *obj, PyObject *name) | |
{ | |
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0); | |
} | |
int | |
_PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, | |
PyObject *value, PyObject *dict) | |
{ | |
PyTypeObject *tp = Py_TYPE(obj); | |
PyObject *descr; | |
descrsetfunc f; | |
PyObject **dictptr; | |
int res = -1; | |
if (!PyUnicode_Check(name)){ | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return -1; | |
} | |
if (tp->tp_dict == NULL && PyType_Ready(tp) < 0) | |
return -1; | |
Py_INCREF(name); | |
descr = _PyType_Lookup(tp, name); | |
if (descr != NULL) { | |
Py_INCREF(descr); | |
f = Py_TYPE(descr)->tp_descr_set; | |
if (f != NULL) { | |
res = f(descr, obj, value); | |
goto done; | |
} | |
} | |
/* XXX [Steve Dower] These are really noisy - worth it? */ | |
/*if (PyType_Check(obj) || PyModule_Check(obj)) { | |
if (value && PySys_Audit("object.__setattr__", "OOO", obj, name, value) < 0) | |
return -1; | |
if (!value && PySys_Audit("object.__delattr__", "OO", obj, name) < 0) | |
return -1; | |
}*/ | |
if (dict == NULL) { | |
dictptr = _PyObject_GetDictPtr(obj); | |
if (dictptr == NULL) { | |
if (descr == NULL) { | |
PyErr_Format(PyExc_AttributeError, | |
"'%.100s' object has no attribute '%U'", | |
tp->tp_name, name); | |
} | |
else { | |
PyErr_Format(PyExc_AttributeError, | |
"'%.50s' object attribute '%U' is read-only", | |
tp->tp_name, name); | |
} | |
goto done; | |
} | |
res = _PyObjectDict_SetItem(tp, dictptr, name, value); | |
} | |
else { | |
Py_INCREF(dict); | |
if (value == NULL) | |
res = PyDict_DelItem(dict, name); | |
else | |
res = PyDict_SetItem(dict, name, value); | |
Py_DECREF(dict); | |
} | |
if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError)) | |
PyErr_SetObject(PyExc_AttributeError, name); | |
done: | |
Py_XDECREF(descr); | |
Py_DECREF(name); | |
return res; | |
} |
它做几件事情:
- 在类型变量中通过 MRO 搜索属性值
- 如果值类型实现了 tp_descr_set,将会调用 tp_descr_set
- 否则,使用新值更到对象的字典
# PyObject_GenericGetAttr
获取属性值比设置复杂一些,实际真正调用的也是另一个函数
PyObject * | |
PyObject_GenericGetAttr(PyObject *obj, PyObject *name) | |
{ | |
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0); | |
} | |
PyObject * | |
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, | |
PyObject *dict, int suppress) | |
{ | |
/* Make sure the logic of _PyObject_GetMethod is in sync with | |
this method. | |
When suppress=1, this function suppress AttributeError. | |
*/ | |
PyTypeObject *tp = Py_TYPE(obj); | |
PyObject *descr = NULL; | |
PyObject *res = NULL; | |
descrgetfunc f; | |
Py_ssize_t dictoffset; | |
PyObject **dictptr; | |
if (!PyUnicode_Check(name)){ | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return NULL; | |
} | |
Py_INCREF(name); | |
if (tp->tp_dict == NULL) { | |
if (PyType_Ready(tp) < 0) | |
goto done; | |
} | |
descr = _PyType_Lookup(tp, name); | |
f = NULL; | |
if (descr != NULL) { | |
Py_INCREF(descr); | |
f = Py_TYPE(descr)->tp_descr_get; | |
if (f != NULL && PyDescr_IsData(descr)) { | |
res = f(descr, obj, (PyObject *)Py_TYPE(obj)); | |
if (res == NULL && suppress && | |
PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
goto done; | |
} | |
} | |
if (dict == NULL) { | |
/* Inline _PyObject_GetDictPtr */ | |
dictoffset = tp->tp_dictoffset; | |
if (dictoffset != 0) { | |
if (dictoffset < 0) { | |
Py_ssize_t tsize = Py_SIZE(obj); | |
if (tsize < 0) { | |
tsize = -tsize; | |
} | |
size_t size = _PyObject_VAR_SIZE(tp, tsize); | |
_PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX); | |
dictoffset += (Py_ssize_t)size; | |
_PyObject_ASSERT(obj, dictoffset > 0); | |
_PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0); | |
} | |
dictptr = (PyObject **) ((char *)obj + dictoffset); | |
dict = *dictptr; | |
} | |
} | |
if (dict != NULL) { | |
Py_INCREF(dict); | |
res = PyDict_GetItemWithError(dict, name); | |
if (res != NULL) { | |
Py_INCREF(res); | |
Py_DECREF(dict); | |
goto done; | |
} | |
else { | |
Py_DECREF(dict); | |
if (PyErr_Occurred()) { | |
if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
else { | |
goto done; | |
} | |
} | |
} | |
} | |
if (f != NULL) { | |
res = f(descr, obj, (PyObject *)Py_TYPE(obj)); | |
if (res == NULL && suppress && | |
PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
} | |
goto done; | |
} | |
if (descr != NULL) { | |
res = descr; | |
descr = NULL; | |
goto done; | |
} | |
if (!suppress) { | |
PyErr_Format(PyExc_AttributeError, | |
"'%.50s' object has no attribute '%U'", | |
tp->tp_name, name); | |
} | |
done: | |
Py_XDECREF(descr); | |
Py_DECREF(name); | |
return res; | |
} |
该算法的主要逻辑:
- 在类型变量通过遍历 MRO 查找属性
- 如果值是一个描述符,通过调用__get__并返回调用结果,否则保存该值,继续往下走
- 定位对象的字典。如果字典包含该值,则返回
- 如果步骤 2 中的值是一个描述符,其类型实现了 tp_descr_get,调用 tp_descr_get 并返回调用结果
- 返回步骤 2 中的值。该值可以为 NULL
由于一个属性既可以是实例变量,也可以是类型变量,所以 CPython 必须决定哪个属性优先于另一个属性,该算法实际实现了一个优先顺序:
- 类型数据描述符
- 实例变量
- 类型非数据描述符和其它类型变量
那么为什么 CPython 会让数据描述符优先于实例变量,而非数据描述符不优先?,这个话题单独开另一篇文章来唠唠🐱👤
# Metatype attribute management
基本上,类型的属性就像普通对象的属性一样工作。当我们将一个类型的属性设置为某个值时,CPython 会将该值放入类型的 dictionary 中
>>> B.x = "class x attribute" | |
>>> B.__dict__ | |
mappingproxy({'__module__': '__main__', '__doc__': None, 'x': 'class x attribute'}) |
当我们得到属性的值时,CPython 从类型的 dictionary 中查找它
>>> B.x | |
'class x attribute' |
那如果类型的 dictionary 不包含该属性,CPython 将从 metatype 的 dictionary 中查找它
>>> B.__class__ | |
<class 'type'> | |
>>> B.__class__ is object.__class__ | |
True |
最后,如果 metatype 的 dictionary 中也找不到该属性,CPython 将在 metatype 父类的 dictionary 中查找该值。
type 实现了自己的 tp_getattro 和 tp_setattro
# type_setattro
static int | |
type_setattro(PyTypeObject *type, PyObject *name, PyObject *value) | |
{ | |
int res; | |
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { | |
PyErr_Format( | |
PyExc_TypeError, | |
"can't set attributes of built-in/extension type '%s'", | |
type->tp_name); | |
return -1; | |
} | |
if (PyUnicode_Check(name)) { | |
if (PyUnicode_CheckExact(name)) { | |
if (PyUnicode_READY(name) == -1) | |
return -1; | |
Py_INCREF(name); | |
} | |
else { | |
name = _PyUnicode_Copy(name); | |
if (name == NULL) | |
return -1; | |
} | |
#ifdef INTERN_NAME_STRINGS | |
if (!PyUnicode_CHECK_INTERNED(name)) { | |
PyUnicode_InternInPlace(&name); | |
if (!PyUnicode_CHECK_INTERNED(name)) { | |
PyErr_SetString(PyExc_MemoryError, | |
"Out of memory interning an attribute name"); | |
Py_DECREF(name); | |
return -1; | |
} | |
} | |
#endif | |
} | |
else { | |
/* Will fail in _PyObject_GenericSetAttrWithDict. */ | |
Py_INCREF(name); | |
} | |
res = _PyObject_GenericSetAttrWithDict((PyObject *)type, name, value, NULL); | |
if (res == 0) { | |
/* Clear the VALID_VERSION flag of 'type' and all its | |
subclasses. This could possibly be unified with the | |
update_subclasses() recursion in update_slot(), but carefully: | |
they each have their own conditions on which to stop | |
recursing into subclasses. */ | |
PyType_Modified(type); | |
if (is_dunder_name(name)) { | |
res = update_slot(type, name); | |
} | |
assert(_PyType_CheckConsistency(type)); | |
} | |
Py_DECREF(name); | |
return res; | |
} |
该函数通过调用_PyObject_GenericSetAttrWithDict 来设置属性值,但它也执行了其他操作。首先,它确保类型不是静态定义的类型,因为这些类型被设计为 immutable。
>>> int.x = 1 | |
Traceback (most recent call last): | |
File "<stdin>", line 1, in <module> | |
TypeError: can't set attributes of built-in/extension type 'int' |
除此之外,它还会检测属性是否为一个特殊方法,如果属性是一个特殊方法,它将更新与该特殊方法对应的 slot
# type_getattro
/* This is similar to PyObject_GenericGetAttr(), | |
but uses _PyType_Lookup() instead of just looking in type->tp_dict. */ | |
static PyObject * | |
type_getattro(PyTypeObject *type, PyObject *name) | |
{ | |
PyTypeObject *metatype = Py_TYPE(type); | |
PyObject *meta_attribute, *attribute; | |
descrgetfunc meta_get; | |
PyObject* res; | |
if (!PyUnicode_Check(name)) { | |
PyErr_Format(PyExc_TypeError, | |
"attribute name must be string, not '%.200s'", | |
Py_TYPE(name)->tp_name); | |
return NULL; | |
} | |
/* Initialize this type (we'll assume the metatype is initialized) */ | |
if (type->tp_dict == NULL) { | |
if (PyType_Ready(type) < 0) | |
return NULL; | |
} | |
/* No readable descriptor found yet */ | |
meta_get = NULL; | |
/* Look for the attribute in the metatype */ | |
meta_attribute = _PyType_Lookup(metatype, name); | |
if (meta_attribute != NULL) { | |
Py_INCREF(meta_attribute); | |
meta_get = Py_TYPE(meta_attribute)->tp_descr_get; | |
if (meta_get != NULL && PyDescr_IsData(meta_attribute)) { | |
/* Data descriptors implement tp_descr_set to intercept | |
* writes. Assume the attribute is not overridden in | |
* type's tp_dict (and bases): call the descriptor now. | |
*/ | |
res = meta_get(meta_attribute, (PyObject *)type, | |
(PyObject *)metatype); | |
Py_DECREF(meta_attribute); | |
return res; | |
} | |
} | |
/* No data descriptor found on metatype. Look in tp_dict of this | |
* type and its bases */ | |
attribute = _PyType_Lookup(type, name); | |
if (attribute != NULL) { | |
/* Implement descriptor functionality, if any */ | |
Py_INCREF(attribute); | |
descrgetfunc local_get = Py_TYPE(attribute)->tp_descr_get; | |
Py_XDECREF(meta_attribute); | |
if (local_get != NULL) { | |
/* NULL 2nd argument indicates the descriptor was | |
* found on the target object itself (or a base) */ | |
res = local_get(attribute, (PyObject *)NULL, | |
(PyObject *)type); | |
Py_DECREF(attribute); | |
return res; | |
} | |
return attribute; | |
} | |
/* No attribute found in local __dict__ (or bases): use the | |
* descriptor from the metatype, if any */ | |
if (meta_get != NULL) { | |
PyObject *res; | |
res = meta_get(meta_attribute, (PyObject *)type, | |
(PyObject *)metatype); | |
Py_DECREF(meta_attribute); | |
return res; | |
} | |
/* If an ordinary attribute was found on the metatype, return it now */ | |
if (meta_attribute != NULL) { | |
return meta_attribute; | |
} | |
/* Give up */ | |
PyErr_Format(PyExc_AttributeError, | |
"type object '%.50s' has no attribute '%U'", | |
type->tp_name, name); | |
return NULL; | |
} |
有三个重要的区别:
- 它通过 tp_dict 获取类型的字典。泛型实现将尝试使用 metatype 的 tp_dictoffset 来定位它
- 它不仅在类型的字典中搜索类型变量,而且还在类型父类的字典中搜索类型变量。泛型实现将处理像普通对象一样没有继承概念的类型
- 它支持描述符
因此有以下优先顺序:
- 元类型数据描述符
- 类型描述符和其它类型变量
- 元类型非数据描述符和其它元类型变量
type 实现了 tp_getattro 和 tp_setattro,因为 type 是所有内置类型的 metatype,默认情况下是所有类的 metatype,所以大多数类型的属性都是根据这个实现工作的。
类本身默认情况下使用通用实现,如果我们想更改类实例的属性行为或类的属性行为,我们需要定义一个新的类或者一个使用自定义实现的 metatype。
# Custom attribute management
类的 tp_getattro 和 tp_setattro 是由创建新类的 type_new 函数里面设置的。泛型实现是其默认选择。类可以通过定义 getattribute、getattr、setattr 和 delattr 魔法方法来定义属性的访问、赋值和删除。
当一个类定义了__setattr__或__delattr__时,它的 tp_setattro slot 会被设置为 slot_tp_setattro 函数。当一个类定义了__getattribute__或__getattr__时,它的 tp_getattro slot 会被设置为 slot_tp_getattr_hook 函数。
slot_tp_setattro 函数只调用__delattr__(instance, attr_name) 或__setattr__(instance, attr_name, value),这取决于 value 是否为 NULL
static int | |
slot_tp_setattro(PyObject *self, PyObject *name, PyObject *value) | |
{ | |
PyObject *stack[3]; | |
PyObject *res; | |
_Py_IDENTIFIER(__delattr__); | |
_Py_IDENTIFIER(__setattr__); | |
stack[0] = self; | |
stack[1] = name; | |
if (value == NULL) { | |
res = vectorcall_method(&PyId___delattr__, stack, 2); | |
} | |
else { | |
stack[2] = value; | |
res = vectorcall_method(&PyId___setattr__, stack, 3); | |
} | |
if (res == NULL) | |
return -1; | |
Py_DECREF(res); | |
return 0; | |
} |
__getattribute__和__getattr__魔法方法提供了一种定制的属性访问方法。两者都以一个实例和一个属性名作为参数,并返回属性值,区别在于何时调用它们。
__getattr__与__getattribute__或泛型函数一起使用。当__getattribute__或泛型函数引发 AttributeError 时调用它。实现逻辑在 slot_tp_getattr_hook 函数
static PyObject * | |
slot_tp_getattr_hook(PyObject *self, PyObject *name) | |
{ | |
PyTypeObject *tp = Py_TYPE(self); | |
PyObject *getattr, *getattribute, *res; | |
_Py_IDENTIFIER(__getattr__); | |
/* speed hack: we could use lookup_maybe, but that would resolve the | |
method fully for each attribute lookup for classes with | |
__getattr__, even when the attribute is present. So we use | |
_PyType_Lookup and create the method only when needed, with | |
call_attribute. */ | |
getattr = _PyType_LookupId(tp, &PyId___getattr__); | |
if (getattr == NULL) { | |
/* No __getattr__ hook: use a simpler dispatcher */ | |
tp->tp_getattro = slot_tp_getattro; | |
return slot_tp_getattro(self, name); | |
} | |
Py_INCREF(getattr); | |
/* speed hack: we could use lookup_maybe, but that would resolve the | |
method fully for each attribute lookup for classes with | |
__getattr__, even when self has the default __getattribute__ | |
method. So we use _PyType_Lookup and create the method only when | |
needed, with call_attribute. */ | |
getattribute = _PyType_LookupId(tp, &PyId___getattribute__); | |
if (getattribute == NULL || | |
(Py_IS_TYPE(getattribute, &PyWrapperDescr_Type) && | |
((PyWrapperDescrObject *)getattribute)->d_wrapped == | |
(void *)PyObject_GenericGetAttr)) | |
res = PyObject_GenericGetAttr(self, name); | |
else { | |
Py_INCREF(getattribute); | |
res = call_attribute(self, getattribute, name); | |
Py_DECREF(getattribute); | |
} | |
if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
PyErr_Clear(); | |
res = call_attribute(self, getattr, name); | |
} | |
Py_DECREF(getattr); | |
return res; | |
} |
代码做了几件事:
- 如果类没有定义__getattr__,首先设置它的 tp_getattro 为 slot_tp_getattro 函数,然后调用该函数并返回调用结果
- 如果类定义了__getattribute__,就调用该函数并返回调用结果
- 如果来自上一步的调用引发了 AttributeError,就调用__getattr__
- 返回最后一次调用的结果
slot_tp_getattr 函数是当类定义了__getattribute__而不是__getattr__时,CPython 使用 tp_getattro slot 的实现。这个函数只调用__getattribute__
/* There are two slot dispatch functions for tp_getattro. | |
- slot_tp_getattro() is used when __getattribute__ is overridden | |
but no __getattr__ hook is present; | |
- slot_tp_getattr_hook() is used when a __getattr__ hook is present. | |
The code in update_one_slot() always installs slot_tp_getattr_hook(); this | |
detects the absence of __getattr__ and then installs the simpler slot if necessary. | |
*/ | |
static PyObject * | |
slot_tp_getattro(PyObject *self, PyObject *name) | |
{ | |
PyObject *stack[2] = {self, name}; | |
return vectorcall_method(&PyId___getattribute__, stack, 2); | |
} |
# Loading methods
当我们获取或设置 Python 对象的属性时会发生什么,来探讨下 Python 属性比较重要的方面。
我们可以看到函数对象是一个描述符,当我们将它绑定到一个实例对象时,它返回一个 bound method
>>> a.f | |
<bound method <lambda> of <__main__.A object at 0x00000128F0AEB490>> |
当编译器看到带有像 object.method (args1, arg2, argN) 这样的位置参数的方法调用时,它不会产生加载该方法的 LOAD_ATTR 和调用该方法的 CALL_FUNCTION 操作码,相反,它会生成一对 LOAD_METHOD 和 CALL_METHOD 操作码:
>>> dis.dis("object.method()") | |
1 0 LOAD_NAME 0 (object) | |
2 LOAD_METHOD 1 (method) | |
4 CALL_METHOD 0 | |
6 RETURN_VALUE |
当 VM 看到 LOAD_METHOD 操作码时,它调用_PyObject_GetMethod 函数来搜索属性值。此函数工作方式与泛型函数类似,区别在于它会检查该值是否是未绑定的方法,即返回绑定到实例的类似方法的对象的描述符。在这种情况下,它不调用描述符类型的 tp_descr_get 方法,而是返回描述符本身。例如属性值是一个函数,_PyObject_GetMethod 返回该函数。函数类型和其它描述符类型的对象作为未绑定的方法,在它的 tp_flag 中指定了 Py_TPFLAGS_METHOD_DESCRIPTOR flag,所以很容易识别出来。
/* Objects behave like an unbound method */ | |
#define Py_TPFLAGS_METHOD_DESCRIPTOR (1UL << 17) | |
int | |
_PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) | |
{ | |
// ...... | |
int meth_found = 0; | |
// ...... | |
if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr | |
|| !PyUnicode_Check(name)) { | |
*method = PyObject_GetAttr(obj, name); | |
return 0; | |
} | |
// ...... | |
descr = _PyType_Lookup(tp, name); | |
if (descr != NULL) { | |
Py_INCREF(descr); | |
if (_PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) { | |
meth_found = 1; | |
} | |
} | |
// ...... | |
if (meth_found) { | |
*method = descr; | |
return 1; | |
} | |
// ...... | |
} |
需要注意,_PyObject_GetMethod 只有在对象的类型使用 tp_getattro 的通用实现时才能正常工作。否则它只调用自定义实现,不执行任何检查。
如果_PyObject_GetMethod 找到一个未绑定的方法,则必须使用前置于参数列表的实例调用该方法。如果它发现其它不需要绑定到实例的可调用函数,参数列表必须保持不变。因此,在 VM 执行 LOAD_METHOD 之后,堆栈上的值可以以两种方式中的一种进行排列:
- 未绑定的方法和包括实例在内的参数列表:(method | self | arg1 | … | argN)
- 其它可调用参数和没有实例的参数列表: (NULL | method| arg1 | … | argN)
CALL_METHOD 操作码的存在是为了在每种情况下适当地调用该方法
# Listing attributes of an object
Python 提供内置的 dir 函数,可用于查看对象具有哪些属性。但它是如何找到这些属性的?它通过调用对象类型的__dir__魔法方法来实现。类型很少会定义自己的__dir__,但所有类型都有它,这是因为对象类型定义了__dir__,而所有其它类型都继承了该对象。对象提供的实现列出了存储在对象的字典、类型的字典和类型父类的字典中的所有属性。这时因为 type 提供了自己的__dir__实现。此实现返回存储在类型的字典和类型父类的字典中的属性。但是它们忽略了存储在 metatype 的字典和 metatype 父类的字典中的属性。文档中解释道
Because dir() is supplied primarily as a convenience for use at an interactive prompt, it tries to supply an interesting set of names more than it tries to supply a rigorously or consistently defined set of names, and its detailed behavior may change across releases. For example, metaclass attributes are not in the result list when the argument is a class.
# Where attributes of types come from
>>> dir(object) | |
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] | |
>>> dir(int) | |
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes'] |
对于魔法方法,是由初始化类型的 PyType_Ready 函数自动添加的,那其他属性呢?
指定类型属性的最直接方法是创建一个字典,用属性填充它,并将类型的 tp_dict 设置为该字典。PyType_Ready 函数在运行时创建内置类型的字典,它还负责添加所有属性。
首先,PyType_Ready 确保类型具有字典,然后它向字典添加属性。类型通过指定 tp_method、tp_member 和 tp_getset 来告诉 PyType_Ready 要添加哪些属性。每个 slot 都是一个结构数组,用于描述不同类型的属性。
# tp_method
tp_method 是描述方法的 PyMthodDef 结构数组:
struct PyMethodDef { | |
const char *ml_name; /* The name of the built-in function/method */ | |
PyCFunction ml_meth; /* The C function that implements it */ | |
int ml_flags; /* Combination of METH_xxx flags, which mostly | |
describe the args expected by the C func */ | |
const char *ml_doc; /* The __doc__ attribute, or NULL */ | |
}; | |
typedef struct PyMethodDef PyMethodDef; |
ml_meth member 是一个指向实现该方法的 C 函数指针,它的签名可以是许多签名中的一个。ml_flag 字段用于告诉 CPython 如何准确地调用该函数。
对于 tp_method 中的每个结构,PyType_Ready 将一个可调用对象添加到类型的字典中,此对象封装了结构,当我们调用它时,调用由 ml_meth 指向的函数。这基本上就是 C 函数如何称为 Python 类型方法的过程。
# tp_member
tp_member slot 是 PyMemberDef 结构的数组。每个结构描述一个属性,该属性公开该类型对象的一个 C 成员:
typedef struct PyMemberDef { | |
const char *name; | |
int type; | |
Py_ssize_t offset; | |
int flags; | |
const char *doc; | |
} PyMemberDef; |
成员由偏移量指定,其类型由类型指定。
对于 tp_member 中的每个结构,PyType_Ready 将一个成员描述符添加到类型的字典中。成员描述符是封装 PyMemberDef 的数据描述法。它的 tp_descr_get 接受一个实例,找到位于偏移位置的实例的成员,将其转换为相应的 Python 对象并返回。它的 tp_descr_set 接受一个实例和一个值,找到位于偏移量的实例的成员,并将其设置为该值的 C 等价物。可以通过 flag 指定成员是否可读可写
例如通过这种机制,type 定义__dictoffset__和其他成员:
static PyMemberDef type_members[] = { | |
{"__basicsize__", T_PYSSIZET, offsetof(PyTypeObject,tp_basicsize),READONLY}, | |
{"__itemsize__", T_PYSSIZET, offsetof(PyTypeObject, tp_itemsize), READONLY}, | |
{"__flags__", T_ULONG, offsetof(PyTypeObject, tp_flags), READONLY}, | |
{"__weakrefoffset__", T_PYSSIZET, | |
offsetof(PyTypeObject, tp_weaklistoffset), READONLY}, | |
{"__base__", T_OBJECT, offsetof(PyTypeObject, tp_base), READONLY}, | |
{"__dictoffset__", T_PYSSIZET, | |
offsetof(PyTypeObject, tp_dictoffset), READONLY}, | |
{"__mro__", T_OBJECT, offsetof(PyTypeObject, tp_mro), READONLY}, | |
{0} | |
}; |
# tp_getset
tp_getset 是 PyGetSetDef 结构的数组,它描述任意的数据描述符,如 property:
typedef struct PyGetSetDef { | |
const char *name; | |
getter get; | |
setter set; | |
const char *doc; | |
void *closure; | |
} PyGetSetDef; |
对于 tp_getset 中的每个结构,PyType_Ready 将一个 getset 描述符添加到类型的字典中,getset 描述符的 tp_descr_get 指定 get 函数,tp_descr_set 指定 set 函数。
类型使用这种机制定义__dict__属性
static PyGetSetDef func_getsetlist[] = { | |
{"__code__", (getter)func_get_code, (setter)func_set_code}, | |
{"__defaults__", (getter)func_get_defaults, | |
(setter)func_set_defaults}, | |
{"__kwdefaults__", (getter)func_get_kwdefaults, | |
(setter)func_set_kwdefaults}, | |
{"__annotations__", (getter)func_get_annotations, | |
(setter)func_set_annotations}, | |
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, | |
{"__name__", (getter)func_get_name, (setter)func_set_name}, | |
{"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname}, | |
{NULL} /* Sentinel */ | |
}; |
__dict__属性不是作为只读成员描述符实现,而是作为 getset 描述符实现,因为它不仅仅返回位于 tp_dictoffset 的字典。例如描述符创建字典(如果它还不存在)
类还通过这种机制获取__dict__属性。创建类的 type_new 函数在调用 PyType_Ready 之前指定 tp_getset。但是有些类没有这个属性,因为它们的实例没有字典,这些是定义了__slots__的类
# __slots__
类的__slots__属性枚举该类可以拥有的属性
>>> class D: | |
... __slots__ = ('x', 'y') | |
... |
如果一个类定义了__slots__,那么__dict__属性将不会被添加到类的字典中,并且该类的 tp_dictoffset 被设置为 0。这样做的主要结果是实例没有字典
>>> D.__dictoffset__ | |
0 | |
>>> d = D() | |
>>> d.__dict__ | |
Traceback (most recent call last): | |
File "<stdin>", line 1, in <module> | |
AttributeError: 'D' object has no attribute '__dict__' |
但是__slots__中列出的属性可以正常的工作
>>> d.x = 1 | |
>>> d.x | |
1 |
会发现很奇怪,定义在__slots__的成员成为了类实例的成员。对于每个成员,成员描述符被添加到类字典中。type_new 函数指定 tp_memeber 执行此操作。
>>> D.x | |
<member 'x' of 'D' objects> |
你会发现 x、y 是一个 member object。实例没有字典,所以__slots__可以节省内存。
Descriptor HowTo Guide
编译器生成 LOAD_ATTR、STORE_ATTR 和 DELETE_ATTR 操作码以获取、设置和删除属性。为了执行这些操作码,VM 调用对象类型的 tp_getattro 和 tp_setattro slot,一个类型可能以任意的方式实现这些 slot,但大多数情况下我们必须处理三个实现:
- 大多数内置类型和类使用的泛型实现
- 按类型使用的实现
- 定义__getattribute__、__getattr__、__setattr__和__delattr__魔法方法的类所使用的实现
简而言之,描述符是控制属性访问、分配和删除的属性。它允许 CPython 实现许多特性,包括方法和属性。
内置类型使用三种机制定义属性:
- tp_methods
- tp_members
- tp_getset
类还是用这些机制来定义一些属性,比如__dict__被定义为一个 getset 描述符,而__slots__中定义的属性被定义为成员描述符。