【问题标题】:Why do __setattr__ and __delattr__ raise an AttributeError in this case?为什么 __setattr__ 和 __delattr__ 在这种情况下会引发 AttributeError ?
【发布时间】:2021-06-19 13:43:10
【问题描述】:

在 Python 中,如果类型有一个属性是 data 没有__set__ 方法的描述符?同样,如果类型的属性是 data 描述符而没有__delete__方法?

我之所以问这个问题是因为我注意到 object.__getattribute__type.__getattribute__ 在属性 lookup 期间确实不会引发AttributeError,如果该类型具有以下属性是一个没有__get__ 方法的数据 描述符。

这是一个简单的程序,说明一方面object.__getattribute__ 查找属性(AttributeError 未引发)与object.__setattr__ 更新属性和object.__delattr__ 删除属性之间的区别( AttributeError 被提出):

class DataDescriptor1:  # missing __get__
    def __set__(self, instance, value): pass
    def __delete__(self, instance): pass

class DataDescriptor2:  # missing __set__
    def __get__(self, instance, owner=None): pass
    def __delete__(self, instance): pass

class DataDescriptor3:  # missing __delete__
    def __get__(self, instance, owner=None): pass
    def __set__(self, instance, value): pass

class A:
    x = DataDescriptor1()
    y = DataDescriptor2()
    z = DataDescriptor3()

a = A()
vars(a).update({'x': 'foo', 'y': 'bar', 'z': 'baz'})

a.x
# actual: returns 'foo'
# expected: returns 'foo'

a.y = 'qux'
# actual: raises AttributeError: __set__
# expected: vars(a)['y'] == 'qux'

del a.z
# actual: raises AttributeError: __delete__
# expected: 'z' not in vars(a)

这是另一个简单的程序,说明一方面type.__getattribute__ 查找属性(AttributeError 未引发)与type.__setattr__ 属性更新和type.__delattr__ 属性删除之间的区别( AttributeError 被提出):

class DataDescriptor1:  # missing __get__
    def __set__(self, instance, value): pass
    def __delete__(self, instance): pass

class DataDescriptor2:  # missing __set__
    def __get__(self, instance, owner=None): pass
    def __delete__(self, instance): pass

class DataDescriptor3:  # missing __delete__
    def __get__(self, instance, owner=None): pass
    def __set__(self, instance, value): pass

class M(type):
    x = DataDescriptor1()
    y = DataDescriptor2()
    z = DataDescriptor3()

class A(metaclass=M):
    x = 'foo'
    y = 'bar'
    z = 'baz'

A.x
# actual: returns 'foo'
# expected: returns 'foo'

A.y = 'qux'
# actual: raises AttributeError: __set__
# expected: vars(A)['y'] == 'qux'

del A.z
# actual: raises AttributeError: __delete__
# expected: 'z' not in vars(A)

我希望实例字典发生突变,而不是获取AttributeError 用于属性更新和属性删除。属性查找从实例字典返回一个值,所以我想知道为什么属性更新和属性删除也不使用实例字典(就像如果类型没有作为数据描述符的属性时他们会这样做)。

【问题讨论】:

    标签: python attributes lookup python-descriptors python-datamodel


    【解决方案1】:

    我认为这只是 C 级设计的结果,没有人真正考虑或关心太多。

    在C级,__set____delete__对应同一个C级slottp_descr_set,通过传递一个空值来设置删除。 (这类似于用于__setattr____delattr__ 的设计,这也对应于a single slot,它也通过NULL 进行删除。)

    如果您实现__set____delete__,C 级槽将设置为wrapper function,它会查找__set____delete__ 并调用它:

    static int
    slot_tp_descr_set(PyObject *self, PyObject *target, PyObject *value)
    {
        PyObject* stack[3];
        PyObject *res;
        _Py_IDENTIFIER(__delete__);
        _Py_IDENTIFIER(__set__);
    
        stack[0] = self;
        stack[1] = target;
        if (value == NULL) {
            res = vectorcall_method(&PyId___delete__, stack, 2);
        }
        else {
            stack[2] = value;
            res = vectorcall_method(&PyId___set__, stack, 3);
        }
        if (res == NULL)
            return -1;
        Py_DECREF(res);
        return 0;
    }
    

    slot 没办法说“哎呀,没找到方法,回到正常处理”,也没有尝试。它也不会尝试模拟正常处理——这很容易出错,因为“正常处理”是依赖于类型的,它不知道要为所有类型模拟什么。如果 slot wrapper 没有找到该方法,它只会引发异常。

    如果__set____delete__ 获得了两个插槽,则不会发生这种效果,但有人在设计 API 时必须小心,我怀疑是否有人这样做。

    【讨论】:

    • 正如您似乎建议的那样,根本问题似乎是函数 slot_tp_descr_set 在查找 __set____delete__ 方法不存在时以及在查找时返回 -1 __set____delete__ 方法存在,但其调用引发异常(通常为 AttributeError)。所以调用函数_PyObject_GenericSetAttrWithDict没有办法区分,回退到前一种情况下的实例上。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 2017-08-19
    • 1970-01-01
    • 2021-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多