Почему в этом случае __setattr__ и __delattr__ вызывают AttributeError?

В Python, каково обоснование того, что object.__setattr__ и type.__setattr__ вызывают AttributeError во время атрибута update, если у типа есть атрибут, который является дескриптором data без метода __set__? Аналогичным образом, в чем причина того, что object.__delattr__ и type.__delattr__ вызывают AttributeError во время удаления атрибута, если у типа есть атрибут, который является дескриптором data без метода __delete__?

Я спрашиваю об этом, потому что заметил, что object.__getattribute__ и type.__getattribute__ не вызывают 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 для обновления атрибута и удаления атрибута. Поиск атрибута возвращает значение из словаря экземпляра, поэтому мне интересно, почему обновление атрибута и удаление атрибута также не используют словарь экземпляра (как они делали бы, если бы у типа не было атрибута, который является дескриптором данных).


person Maggyero    schedule 22.03.2021    source источник


Ответы (1)


Я думаю, что это просто следствие дизайна C-уровня, о котором никто не думал и не заботился.

На уровне C __set__ и __delete__ соответствуют одному и тому же уровню C slot, tp_descr_set и удаление задаются путем передачи значения null в set. (Это похоже на дизайн, используемый для __setattr__ и __delattr__, которые также соответствуют отдельный слот, который также передается NULL для удаления.)

Если вы реализуете либо __set__, либо __delete__, слот уровня C устанавливается в функция-оболочка, которая ищет __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;
}

У слота нет возможности сказать ой, метод не найден, вернуться к обычному обращению, и он не пытается. Он также не пытается эмулировать нормальную обработку — это было бы подвержено ошибкам, поскольку нормальная обработка зависит от типа, и он не может знать, что эмулировать для всех типов. Если оболочка слота не находит метод, она просто вызывает исключение.

Этого эффекта не произошло бы, если бы __set__ и __delete__ получили два слота, но кому-то нужно было позаботиться об этом, пока они разрабатывали API, и я сомневаюсь, что кто-то это сделал.

person user2357112 supports Monica    schedule 22.03.2021
comment
Как вы, кажется, предполагаете, основная проблема заключается в том, что функция slot_tp_descr_set возвращает -1 как при отсутствии искомого метода __set__ или __delete__, так и при наличии искомого метода __set__ или __delete__, но его вызов вызывает исключение (часто AttributeError). Таким образом, вызывающая функция _PyObject_GenericSetAttrWithDict не может чтобы сделать различие и вернуться к экземпляру в первом случае. - person Maggyero; 27.03.2021