pythondelegationgetattrslotssetattr

Using __setattr__ and __getattr__ for delegation with __slots__ without triggering infinite recursion


class A:
    __slots__ = ("a",)
    def __init__(self) -> None:
        self.a = 1

class B1:
    __slots__ = ("b",)
    def __init__(self, b) -> None:
        self.b = b

    def __getattr__(self, k):
        return getattr(self.b, k)

    def __setattr__(self, k, v):
        setattr(self.b, k, v)

class B2:
    __slots__ = ("b",)
    def __init__(self, b) -> None:
        self.b = b

    def __getattr__(self, k):
        return getattr(super().__getattr__("b"), k)

    def __setattr__(self, k, v):
        setattr(super().__getattr__("b"), k, v)

class B3:
    __slots__ = ("b",)
    def __init__(self, b) -> None:
        self.b = b

    def __getattr__(self, k):
        return getattr(getattr(super(), "b"), k)

    def __setattr__(self, k, v):
        setattr(getattr(super(), "b"), k, v)

a = A()
b = B1(a)
print(b.a) # RecursionError: maximum recursion depth exceeded

b = B2(a)
print(b.a) # AttributeError: 'super' object has no attribute '__getattr__'

b = B3(a)
print(b.a) # AttributeError: 'super' object has no attribute 'b'

Solution

  • A more proper way is to check if the attribute name is in any of the available __slots__ up the class hierarchy before delegating:

    class BCorrect(object):
        __slots__ = ('b',)
    
        def __init__(self, b) -> None:
            self.b = b
    
        def _in_slots(self, attr) -> bool:
            for cls in type(self).__mro__:
                if attr in getattr(cls, '__slots__', []):
                    return True
            return False
    
        def __getattr__(self, attr):
            if self._in_slots(attr):
                return object.__getattr__(self, attr)
            return getattr(self.b, attr)
    
        def __setattr__(self, attr, value):
            if self._in_slots(attr):
                object.__setattr__(self, attr, value)
                return
            setattr(self.b, attr, value)
    

    This has the advantages that it does not break inheritance and does not need any magic in __init__.