I have a class that uses __slots__ and makes them nearly immutable by overriding __setattr__ to always raise an error:
class A:
__slots__ = ['a', 'b', '_x']
def __init__(self, a, b):
object.__setattr__(self, 'a', a)
object.__setattr__(self, 'b', b)
def __setattr__(self, attr, value):
raise AttributeError('Immutable!')
@property
def x():
return self._x
@x.setter
def x(value):
object.__setattr__(self, '_x', value)
Here, the "private" attribute _x is a place-holder for a complex operation to interact with some custom hardware.
Since x is a property, I expect to be able to do something like
inst = A(1, 2)
inst.x = 3
Instead, I see my AttributeError with the message Immutable!.
There are a number of obvious workarounds here, such as to remove the custom __setattr__ (which I do not want to do) or to rewrite it as
def __setattr__(self, attr, value):
if attr != 'x':
raise AttributeError('Immutable!')
super().__setattr__(attr, value)
This seems like an awkward method that has the potential to balloon out of proportion if I start adding more properties like that.
The real issue is that I do not understand why there is no conflict between __slots__ and the property, but there is one between __setattr__ and the property. What is happening with the lookup order, and is there another, more elegant workaround to this problem?
The real issue is that I do not understand why there is no conflict between
__slots__and the property, but there is one between__setattr__and the property.
Both __slots__ and property implement attribute lookup by providing a descriptor for the corresponding attribute(s). The presence of __slots__ prevents arbitrary instance attribute creation not by doing anything to __setattr__, but by preventing creation of a __dict__. property and other descriptors don't rely on an instance __dict__, so they're unaffected.
However, __setattr__ handles all attribute assignment, meaning that descriptor invocation is __setattr__'s responsibility. If your __setattr__ doesn't handle descriptors, descriptors won't be handled, and property setters won't be invoked.
is there another, more elegant workaround to this problem?
You could explicitly allow only properties:
class A:
...
def __setattr__(self, name, value):
if not isinstance(getattr(type(self), name, None), property):
raise AttributeError("Can't assign to attribute " + name)
super().__setattr__(name, value)
or you could explicitly reject assignment to slots, and delegate other attribute assignment to super().__setattr__:
class A:
...
def __setattr__(self, name, value):
if isinstance(getattr(type(self), name, None), _SlotDescriptorType):
raise AttributeError("Can't assign to slot " + name)
super().__setattr__(name, value)
# Seems to be the same as types.MemberDescriptorType,
# but the docs don't guarantee it.
_SlotDescriptorType = type(A.a)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With