pythonclasspropertiessetattr

Why __getattr__ triggers __setattr__ in the following python code?


class D:
    def __init__(self):
        self._attr = 1
        self._attr2 = 2

    def __getattr__(self, name):
        if name == 'data':
            return (self._attr, self._attr2)

    def __setattr__(self, name, value):
        print('__setattr__')
        if name == 'data':
            self._attr, self._attr2 = value
        else:            
            super().__setattr__(name, value)

d = D()
print(d.data)  
#OUTPUT:
#__setattr__
#__setattr__
#(1,2)

For some reason the line return (self._attr, self._attr2) behaves as an assignment to self._attr and self._attr2, but why?

I consulted Google and Gemini, the Mark Lutz book, but couldn't figure it out.


Solution

  • It's not coming from the print(d.data) line. Comment it out. It is from setting the attributes in __init__. Extend the print in __setattr__ to print(f'__setattr__({name=}, {value=})') to see more information:

    class D:
        def __init__(self):
            self._attr = 1
            self._attr2 = 2
    
        def __getattr__(self, name):
            if name == 'data':
                return (self._attr, self._attr2)
    
        def __setattr__(self, name, value):
            print(f'__setattr__({name=}, {value=})')
            if name == 'data':
                self._attr, self._attr2 = value
            else:
                super().__setattr__(name, value)
    
    d = D()
    #print(d.data)
    

    Output:

    __setattr__(name='_attr', value=1)
    __setattr__(name='_attr2', value=2)
    

    Note you can also get the same behavior implementing data as a property and have more clear code:

    class D:
        def __init__(self):
            self._attr = 1
            self._attr2 = 2
    
        @property
        def data(self):
            return (self._attr, self._attr2)
    
        @data.setter
        def data(self, value):
            self._attr, self._attr2 = value
    
    d = D()
    print(d.data)
    d.data = 4,5
    print(d.data)
    

    Output:

    (1, 2)
    (4, 5)