【问题标题】:Strange Descriptor Behavior奇怪的描述符行为
【发布时间】:2017-05-24 22:18:01
【问题描述】:

当描述符的__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__ 可调用的第一个参数?也许更重要的是,为什么这种行为在所有描述符属性中都不一致?

这是怎么回事?

【问题讨论】:

    标签: python descriptor


    【解决方案1】:

    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描述符,但现在我们需要考虑MyClass.d描述符的__set____delete__方法本身就是描述符。它们不是,但如果它们是用常规 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_LookupIdtype(mc) 上查找__get__,而不是使用call_methodmc 上查找它。

    call_method 不同,_PyType_LookupId 不进行描述符处理。 Python 假设没有检查,因为它跳过了描述符处理,所以它需要手动绑定self。它显式地将self(即Descriptor 实例)传递给PyObject_CallFunctionObjArgs(get, self, obj, type, NULL) 中的__get__ 方法。


    __get__Descriptor 实例视为 first,因为 Python 在调用 __get__ 时使用错误的快捷方式在内部进行二级描述符处理,但在调用 __set____delete__ 时则不然。

    【讨论】:

    • 很好的描述。据我所知,没有对此的规范,但我可能错了。不管查找方法如何,这里的不一致似乎都不是不合适的。
    猜你喜欢
    • 1970-01-01
    • 2015-07-27
    • 2013-02-01
    • 1970-01-01
    • 2018-01-14
    • 1970-01-01
    • 1970-01-01
    • 2023-03-04
    • 2023-03-03
    相关资源
    最近更新 更多