奇怪的描述符行为

时间:2017-05-24 22:18:02

标签: python descriptor

当描述符的__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>

当此属性在技术上不是“方法”时,为什么所有者描述符传递给__get__的第一个参数可调用?也许更重要的是,为什么这种行为在所有描述符属性中都不一致?

这里发生了什么?

1 个答案:

答案 0 :(得分:4)

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__和实例dict,但执行描述符处理,就像正常的属性查找一样。

请注意,此处有两个级别的描述符处理 - 我们正处理MyClass.d描述符,但现在我们需要考虑__set__或{{1} __delete__描述符的方法本身就是描述符。它们不是,但如果它们是用常规Python函数实现的,它们就是描述符,Python函数的描述符处理会将MyClass.d实例作为其Descriptor的第一个参数绑定或__set__方法。

对于在Python中实现的__delete__,CPython内部使用slot_tp_descr_get,它以不同方式执行特殊方法查找。

__get__

此处,CPython使用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); } _PyType_LookupId上查找__get__,而不是使用type(mc)call_method上查找。

mc不同,call_method不进行描述符处理。 Python假定而不检查,因为它跳过描述符处理,它需要手动绑定_PyType_LookupId。它明确地将selfself实例)传递给Descriptor中的__get__方法。

PyObject_CallFunctionObjArgs(get, self, obj, type, NULL)__get__实例视为Descriptor,因为Python在调用first时会在内部使用错误的快捷方式处理第二级描述符,但在调用{{1}时则不会}或__get__