描述器
这篇文章我们来讲一个大家可能平时主动用的不多,但是在 Python 里非常非常重要的一个结构,叫做描述器。也就是 descriptor。那 descriptor 这个概念大家可能有的听说过,有的没有听说过。但是在 CPython 里实际上有大量的机制是通过 descriptor 来完成的。或许你并不知道,但是实际上你一直在用这个机制。
描述器的定义
好我们先来简单介绍一下 descriptor,那在 Python 层级呢,一个 descriptor 就是你定义了一个 __get__ 函数的 class,或者是定义了 __set__ 或者 __delete__。这三个函数定义任何一个都会把这个 class 变成一个 descriptor。
描述器的基本用法
我们先来看一个 descriptor 最简单的用法,我上面有个 class Name,然后这个 Name 定义了一个 __get__ 函数,这个 __get__ 函数里面的 argument 必须得是一个 object 和一个 object type。然后这个 __get__ 函数很简单,直接 return 了一个 Peter。我们在下面定义了一个 class A,这个 class A 里面有一个属于 class 的 member variable 叫做 name,当然这个 name 就是一个 Name object。那由于我们的 class Name 是一个 descriptor,所以我们这个 class A 里面这个 member name 呢就是一个 descriptor object。那我们在下面建立了一个 class A 的 object o,然后我们打印 o 的 name。我们看一下结果是什么?可以看到我们打印出了 Peter。也就是说当我们使用 o.name 的时候,它实际上是调用了 Name 这个 class 的 __get__ 函数。
Name 定义了 __get__,class A 的 name 是一个 Name object。打印 o.name,调用的是 Name 的 __get__,打印出 Peter。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = Name()o = A()print(o.name)
那我们换一种方式,我们这次打印 class A 的 name,可以看到打印 class A 的 name,它依然返回的是一个 Peter。这里呢,其实你应该已经能感受到一点点的神奇了,因为这个 class A 的 name variable 是被我们赋值的一个 Name object,对吧?
换成打印 class A 的 name,返回的依然是 Peter。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = Name()o = A()# print(o.name)print(A.name)print(o.name)
描述器与实例属性
那你很有可能就会想这个 descriptor 是怎么实现的?因为如果你不把这个事情搞明白,你就不太容易解释下面这个问题。我把代码改了一下,现在呢,这个 A 我给了它一个 __init__ 函数,这个 __init__ 函数里面我做了一个 self.name 等于 Name。下面我们再建立一个 o,然后打印 o.name。可以看到它的结果变了,现在它打印出来的就是这个 Name object 了。
A 改成在 __init__ 里把一个 Name object 赋值给 self.name,这次打印 o.name 得到的是这个 Name object 本身。
class Name:def __get__(self, obj, objtype):return "Peter"class A:def __init__(self):self.name = Name()name = Name()o = A()print(o.name)# print(o.name)print(A.name)
那这是为什么呢?如果你觉得刚才那个问题还不令人困惑的话,我们又把刚才的代码稍微改了一下,在建立这个 object 之后,我们把 o.name 赋值成了 Bob,我们再打印 o.name 会有什么结果呢?可以看到它打印出来的就是我们新赋值的这个变量了。
name 回到 class A 里,建立 o 之后把 o.name 赋值成 Bob,再打印 o.name,得到的是新赋值的 Bob。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = Name()def __init__(self):self.name = Name()o = A()o.name = "Bob"print(o.name)
那如果到这里你还觉得一切理所当然的话,我就放一个大招。在我们把 o.name 赋值成 Bob 之后,我们动态地把 Name 这个 class 的 __set__ 设置成一个没有意义的函数。注意我们这一步只给这个 descriptor 增加了一个 method,而上面对 o.name 的赋值是已经完成了。此时我们再打印 o.name,结果就变回了 Peter。作为一个小调查,上面这些结果的原理你都知道怎么回事吗?
第 9 行把 o.name 赋值成 Bob 之后,第 10 行动态地给 Name 加上一个没有意义的 __set__,第 11 行再打印 o.name,结果变回了 Peter。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = Name()o = A()o.name = "Bob"Name.__set__ = lambda x, y, z: Noneprint(o.name)
属性访问的字节码
那其实呢,所有的故事都要从一个 bytecode 也就是 LOAD_ATTR 说起。这个 bytecode 非常非常常见。因为只要你在 Python 代码中写出,形如 a.b 这种形式的代码的时候,当你在求它的值,就会使用 LOAD_ATTR。当你要在这里保存值就会使用 STORE_ATTR。像在左边第 9 行我们做 o.name 等于 Bob 的时候,我们就使用了一个 STORE_ATTR。而在第 11 行 print o.name 的时候,我们会用一个 LOAD_ATTR。那有关 descriptor 这些秘密都在这两个 bytecode 的源代码里。LOAD_ATTR 跟 STORE_ATTR 有相似的机制。所以我们今天说明白 load,然后 store 的部分,大家如果感兴趣留作家庭作业。
LOAD_ATTR 的调用链
我们看 LOAD_ATTR 的代码比较简单,它的核心是调用一个 PyObject_GetAttr。
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();
}
那 PyObject_GetAttr 里面的核心代码是这个,是 tp_getattro。
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;
}
那在你没有重载的情况下,base object 的 tp_getattro,是 PyObject_GenericGetAttr 函数。
PyTypeObject PyBaseObject_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"object", /* tp_name */
sizeof(PyObject), /* tp_basicsize */
0, /* tp_itemsize */
object_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
object_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)_Py_HashPointer, /* tp_hash */
0, /* tp_call */
object_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
object_doc, /* tp_doc */
点进去可以看到,return 是 _PyObject_GenericGetAttrWithDict。那这个函数就是我们的目标函数了。是所有魔法发生的地方。我们 descriptor 的逻辑就在这里面。
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
在 type 上查找描述器
那我们知道 getattr 就是一个 object 上面,拿一个 attr。这个 object 是作为 obj 被传进来的。当然 attribute 的名字是保存在 name 里。比如说我们刚才的 o.name,那这个 object 就是 object 的 o 本身。那这个 name 里面就保存了一个 string name。在 1175 行,它首先把 object 的 type 给存到了 tp 这个 pointer 里。对于我们刚才那段代码,o 的 type 就是 class A。
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 suppresses AttributeError.
*/
PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr;
1195 行这是第一个关键点,注意它在这个 type 里在找那个 name。也就是说你这个 descriptor 必须要定义到 type 上,也就是必须要写到 class 的 definition 里面才是有效的。这就解释了为什么我刚才尝试在 __init__ 这个函数里面,就是发现打印出来的东西就是那个 object 本身,而没有触发这个 descriptor。要想让这个 descriptor 工作,它必须得是 type 的一个 attribute,或者说一个 member。你就在 type 产生的 object 上做手脚是没有用的。
descr = _PyType_Lookup(tp, name);
当然我们知道这行代码,就是在 type 里面找到了这个 name 对应的 attribute,这个所谓的 descr 也就是 descriptor。它不一定是一个真实的 descriptor,它可能是任何的 object。当然如果没有这个名字,它会返回一个 NULL,那就无所谓了。所以在 1200 行,它首先去寻找了这个可能是 descriptor 的 object,有没有这个 tp_descr_get 函数。这个函数就是我们刚才定义的 __get__ 函数。这个就是我们最开始说的一个 class,定义了 __get__ 函数,就成为一个 descriptor,它的机制,就是在 LOAD_ATTR 里面,它会找一下。
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
f = Py_TYPE(descr)->tp_descr_get;
属性查找的优先级
接下来这个 if statement 注意了,它找到 __get__ 函数之后没完,它还要判断一下这个玩意儿是不是 IsData。而 IsData 的判断就是它有没有 __set__ 函数。只有当这个 descriptor 既有 __get__ 函数,又有 __set__ 函数的时候,它才会在现在立刻执行这个 get 的操作。大家还记不记得刚才我们的 __get__ 函数,它的三个 argument 分别是这个 descriptor 本身,然后 object 和 object type。那在这里面对应的也是 descriptor object 跟它的 type。也就是如果既有 __get__ 函数也有 __set__ 函数,在这里就会返回这个 __get__ 函数的返回值。当然它会把调用这个 descriptor object,和它的 type 传进这个 __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;
}
好我们接着往下看。我们在正常的使用 LOAD_ATTR 的时候,我们传进来的这个默认 dict 就是一个 NULL。那么这一大段函数呢,就是找到这个 object 本身的 __dict__。object 本身的 __dict__,保存了这个 object 里面所有的 member variable。就是你在正常情况下通过 self 点什么什么什么东西,等于什么赋值的这些东西,全都保存在这个 object 的 __dict__ 里面。
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;
}
}
接下来这个部分非常简单。从这个 object 的 dictionary 里面找一找,有没有你要的这个名字,如果有的话返回回去。也就是我们所谓的跟这个 object 绑定的变量。
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;
}
}
}
}
下面还记不记得 f 是什么?f 是那个 __get__ 函数,对不对?这个部分就是如果你的 descriptor 只有 __get__ 函数,没有 __set__ 函数就会在这运行。这里大家看明白优先级了吗?如果 __get__ 跟 __set__ 都有,优先使用 descriptor。如果不是的话,先在自己的 dict 里面找。如果 dict 里面还没有,就尝试用 descriptor 的 __get__。
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
if (res == NULL && suppress &&
PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
goto done;
}
那我们现在就可以来解释这一段代码了。由于我们 Name 的这个 descriptor 只有 __get__ 而没有 __set__,当我们做 o.name 等于 Bob 的时候,我们是把这个 Bob 赋值到了 o 这个 object 上。通过打印 o.__dict__,我们可以看到 o 这个 object 里面,它的 attribute name 对应的值是 Bob。而由于这个 descriptor name 现在只有 __get__ 没有 __set__,所以在我们第 11 行打印 o.name 的时候,它会去这个 dict 里面去,也就是打印出了 Bob。而在第 12 行我们把这个 Name.__set__ 随便定义了一个函数之后,这个 descriptor 在做 load 的时候的优先级就跑到了 dict 上面。所以它就会优先用这个 __get__ 函数,也就打印出了 Peter。
第 10 行打印出 o.__dict__,name 对应的值是 Bob;第 11 行打印出 Bob;第 13 行给 Name 加上 __set__ 之后,第 14 行打印出 Peter。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = Name()o = A()o.name = "Bob"print(o.__dict__)print(o.name)Name.__set__ = lambda x, y, z: Noneprint(o.name)
类属性的查找
那当然在这个 LOAD_ATTR 里面还有最后一个部分,就是如果 descriptor 不是 NULL 的话,直接返回 descriptor。那这个地方呢你可能会没绕过来,说这是什么意思对吧?这是因为它的变量,稍微有点有误导性。还记不记得这个 descriptor 是怎么来的?实际上就是我们从 type 里面通过 MROMethod Resolution Order,方法解析顺序:在一个类和它的父类里查找属性时,依次查找这些类的顺序。 拿到的一个值。它就是一个正常的 object。它是什么都有可能。它可能就是一个 string,是一个 integer,都有可能。而我们在前面已经排除了这个所谓的 descriptor,里面有 __get__ 这个函数的可能性。如果它没有 __get__ 的话,我们就原样返回。
if (descr != NULL) {
res = descr;
descr = NULL;
goto done;
}
那其实这段代码是出现的超级频繁的。我们看左边就是一个极常见的例子,就是我这个 class A 本身有一个 attribute name,叫 Alice。然后我通过 A 新建了一个 object,这个时候我这个 object dict 是空的。我没有任何数据绑定到这个 object 上。而当我在运行 o.name 的时候,它就会从它的 type 里面拿到这个 name 对应的 object,是一个 string Alice。然后它发现这个 string 既没有 __get__ 能力,也没有 __set__ 的能力,同时我在这个 dict 里面又没有找到,所以我就把它直接返回回来了。也就打印出了 Alice。那这个部分实际上实现的,就是我们在一个类的对象里面找东西,找不到再返回这个类找,甚至它的父类找这么一个功能。就这个功能的机制实际上也是刚才那段代码。
class A 的 name 是一个普通的 string Alice。o.__dict__ 是空的,打印 o.name 得到的是 A 上的 Alice。
class Name:def __get__(self, obj, objtype):return "Peter"class A:name = "Alice"name = Name()o = A()o.name = "Bob"print(o.__dict__)print(o.name)Name.__set__ = lambda x, y, z: Noneprint(o.name)
那通过刚才的介绍呢,大家应该可以感觉到,descriptor 这个东西在 Python 里面非常非常重要。尽管你可能很少自己去写一个 descriptor,但是你的代码是无时无刻不在使用这个机制的。你每一次使用什么点什么的时候都会触发这个机制。而在 Python 里有很多我们常见的机制,实际上就是基于 descriptor 来完成的。那这些机制都有什么呢?如果大家感兴趣的话,我们以后再说。