Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python property lookup with custom __setattr__ and __slots__

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?

like image 264
Mad Physicist Avatar asked Dec 04 '25 13:12

Mad Physicist


1 Answers

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)
like image 146
user2357112 supports Monica Avatar answered Dec 06 '25 06:12

user2357112 supports Monica



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!