奇怪的描述符行为
Strange Descriptor Behavior
当描述符的 __get__
、__set__
或 __delete__
属性不是方法,而是通用可调用对象时,该可调用对象的第一个参数不一致:
class Callable(object):
def __call__(self, first, *args, **kwargs):
print(first)
class Descriptor(object):
__set__ = Callable()
__delete__ = Callable()
__get__ = Callable()
class MyClass(object):
d = Descriptor()
mc = MyClass()
mc.d = 1
del mc.d
mc.d
<__main__.MyClass object at 0x10854cda0>
<__main__.MyClass object at 0x10854cda0>
<__main__.Descriptor object at 0x10855f240>
当此属性在技术上不是 "method" 时,为什么将所有者描述符传递给可调用的 __get__
的第一个参数?也许更重要的是,为什么这种行为在所有描述符属性中都不一致?
这是怎么回事?
CPython 内部的相关部分没有得到一致的实现。这可能被认为是一个错误,尽管我不知道 Python 对这种情况的正确描述符处理有何承诺。
我可以准确解释内部发生的事情,但是由于这里有多层描述符处理,所以事情会变得混乱。
对于在 Python 中实现的 __set__
或 __delete__
,CPython 内部使用 slot_tp_descr_set
将其包装在 C 级别。 (是的,这两种方法都有一个 C 函数。)
static int
slot_tp_descr_set(PyObject *self, PyObject *target, PyObject *value)
{
PyObject *res;
_Py_IDENTIFIER(__delete__);
_Py_IDENTIFIER(__set__);
if (value == NULL)
res = call_method(self, &PyId___delete__, "(O)", target);
else
res = call_method(self, &PyId___set__, "(OO)", target, value);
if (res == NULL)
return -1;
Py_DECREF(res);
return 0;
}
这使用 call_method
,它绕过 __getattribute__
、__getattr__
和实例字典,但像普通属性查找一样执行描述符处理。
请注意,这里有两个级别的描述符处理 - 我们正在处理 MyClass.d
描述符,但现在我们需要考虑 __set__
还是 __delete__
MyClass.d
描述符的方法本身就是描述符。它们不是,但如果它们是用常规 Python 函数实现的,它们将是描述符,并且 Python 函数的描述符处理将绑定 Descriptor
实例作为第一个参数它的 __set__
或 __delete__
方法。
对于 Python 中实现的 __get__
,CPython 内部使用 slot_tp_descr_get
,它以不同的方式执行特殊方法查找。
static PyObject *
slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
PyTypeObject *tp = Py_TYPE(self);
PyObject *get;
_Py_IDENTIFIER(__get__);
get = _PyType_LookupId(tp, &PyId___get__);
if (get == NULL) {
/* Avoid further slowdowns */
if (tp->tp_descr_get == slot_tp_descr_get)
tp->tp_descr_get = NULL;
Py_INCREF(self);
return self;
}
if (obj == NULL)
obj = Py_None;
if (type == NULL)
type = Py_None;
return PyObject_CallFunctionObjArgs(get, self, obj, type, NULL);
}
在这里,CPython使用_PyType_LookupId
在type(mc)
上查找__get__
,而不是使用call_method
在[=31]上查找=].
与 call_method
不同,_PyType_LookupId
不进行描述符处理。 Python 假设 没有检查 因为它跳过了描述符处理,所以它需要手动绑定 self
。它显式地将 self
(即 Descriptor
实例)传递给 PyObject_CallFunctionObjArgs(get, self, obj, type, NULL)
中的 __get__
方法。
__get__
将 Descriptor
实例视为 first
,因为 Python 在调用 __get__
时使用了错误的内部二级描述符处理快捷方式,但是不是在调用 __set__
或 __delete__
.
时
当描述符的 __get__
、__set__
或 __delete__
属性不是方法,而是通用可调用对象时,该可调用对象的第一个参数不一致:
class Callable(object):
def __call__(self, first, *args, **kwargs):
print(first)
class Descriptor(object):
__set__ = Callable()
__delete__ = Callable()
__get__ = Callable()
class MyClass(object):
d = Descriptor()
mc = MyClass()
mc.d = 1
del mc.d
mc.d
<__main__.MyClass object at 0x10854cda0>
<__main__.MyClass object at 0x10854cda0>
<__main__.Descriptor object at 0x10855f240>
当此属性在技术上不是 "method" 时,为什么将所有者描述符传递给可调用的 __get__
的第一个参数?也许更重要的是,为什么这种行为在所有描述符属性中都不一致?
这是怎么回事?
CPython 内部的相关部分没有得到一致的实现。这可能被认为是一个错误,尽管我不知道 Python 对这种情况的正确描述符处理有何承诺。
我可以准确解释内部发生的事情,但是由于这里有多层描述符处理,所以事情会变得混乱。
对于在 Python 中实现的 __set__
或 __delete__
,CPython 内部使用 slot_tp_descr_set
将其包装在 C 级别。 (是的,这两种方法都有一个 C 函数。)
static int
slot_tp_descr_set(PyObject *self, PyObject *target, PyObject *value)
{
PyObject *res;
_Py_IDENTIFIER(__delete__);
_Py_IDENTIFIER(__set__);
if (value == NULL)
res = call_method(self, &PyId___delete__, "(O)", target);
else
res = call_method(self, &PyId___set__, "(OO)", target, value);
if (res == NULL)
return -1;
Py_DECREF(res);
return 0;
}
这使用 call_method
,它绕过 __getattribute__
、__getattr__
和实例字典,但像普通属性查找一样执行描述符处理。
请注意,这里有两个级别的描述符处理 - 我们正在处理 MyClass.d
描述符,但现在我们需要考虑 __set__
还是 __delete__
MyClass.d
描述符的方法本身就是描述符。它们不是,但如果它们是用常规 Python 函数实现的,它们将是描述符,并且 Python 函数的描述符处理将绑定 Descriptor
实例作为第一个参数它的 __set__
或 __delete__
方法。
对于 Python 中实现的 __get__
,CPython 内部使用 slot_tp_descr_get
,它以不同的方式执行特殊方法查找。
static PyObject *
slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
PyTypeObject *tp = Py_TYPE(self);
PyObject *get;
_Py_IDENTIFIER(__get__);
get = _PyType_LookupId(tp, &PyId___get__);
if (get == NULL) {
/* Avoid further slowdowns */
if (tp->tp_descr_get == slot_tp_descr_get)
tp->tp_descr_get = NULL;
Py_INCREF(self);
return self;
}
if (obj == NULL)
obj = Py_None;
if (type == NULL)
type = Py_None;
return PyObject_CallFunctionObjArgs(get, self, obj, type, NULL);
}
在这里,CPython使用_PyType_LookupId
在type(mc)
上查找__get__
,而不是使用call_method
在[=31]上查找=].
与 call_method
不同,_PyType_LookupId
不进行描述符处理。 Python 假设 没有检查 因为它跳过了描述符处理,所以它需要手动绑定 self
。它显式地将 self
(即 Descriptor
实例)传递给 PyObject_CallFunctionObjArgs(get, self, obj, type, NULL)
中的 __get__
方法。
__get__
将 Descriptor
实例视为 first
,因为 Python 在调用 __get__
时使用了错误的内部二级描述符处理快捷方式,但是不是在调用 __set__
或 __delete__
.