In Python, what is the rationale for which object.__setattr__ and type.__setattr__ raise an AttributeError during attribute update if the type has an attribute which is a data descriptor without a __set__ method? Likewise, what is the rationale for which object.__delattr__ and type.__delattr__ raise an AttributeError during attribute deletion if the type has an attribute which is a data descriptor without a __delete__ method?
I am asking this because I have noticed that object.__getattribute__ and type.__getattribute__ do not raise an AttributeError during attribute lookup if the type has an attribute which is a data descriptor without a __get__ method.
Here is a simple program illustrating the differences between attribute lookup by object.__getattribute__ on the one hand (AttributeError is not raised), and attribute update by object.__setattr__ and attribute deletion by object.__delattr__ on the other hand (AttributeError is raised):
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)
Here is another simple program illustrating the differences between attribute lookup by type.__getattribute__ on the one hand (AttributeError is not raised), and attribute update by type.__setattr__ and attribute deletion by type.__delattr__ on the other hand (AttributeError is raised):
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)
I would expect the instance dictionary to be mutated instead of getting an AttributeError for attribute update and attribute deletion. Attribute lookup returns a value from the instance dictionary, so I am wondering why attribute update and attribute deletion do not use the instance dictionary as well (like they would do if the type did not have an attribute which is a data descriptor).
I think it's just a consequence of the C-level design that no one really thought or cared much about.
At C level,
__set__and__delete__correspond to the same C-level slot,tp_descr_set, and deletion is specified by passing a null value to set. (This is similar to the design used for__setattr__and__delattr__, which also correspond to a single slot that also gets passedNULLfor deletion.)If you implement either
__set__or__delete__, the C-level slot gets set to a wrapper function that looks for__set__or__delete__and calls it:The slot has no way to say "oops, didn't find the method, go back to normal handling", and it doesn't try. It also doesn't try to emulate the normal handling - that would be error-prone, since "normal handling" is type-dependent, and it can't know what to emulate for all types. If the slot wrapper doesn't find the method, it just raises an exception.
This effect wouldn't happen if
__set__and__delete__had gotten two slots, but someone would have had to care while they were designing the API, and I doubt anyone did.