Search code examples
pythonpython-descriptors

Same attribute name of an object in python descriptor and __init__ function


I was reading about python descriptors and how to use them. I came across an example from the python official doc,

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        value = obj._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value
    
    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj._age = value

class Person:

    age = LoggedAgeAccess()             # Descriptor instance
    
    def __init__(self, name, age):
        self.name = name                # Regular instance attribute
        self.age = age                  # Calls __set__()
    
    def birthday(self):
        self.age += 1                   # Calls both __get__() and __set__()

Here, we are using '_age' as the attribute name in the descriptor instead of 'age' for storing it in the object's dictionary.

The code works fine if, I do

mary = Person('Mary M', 30)
print(vars(mary))

The output is,

INFO:root:Updating 'age' to 30
{'name': 'Mary M', '_age': 30}

When I tried to change the attribute name in the descriptor from '_age' to 'age'. The __set__ function runs continuosly in an infinite loop. (Class Person remaining the same)

For instance,

class LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        value = obj.age
        logging.info('Accessing %r giving %r', 'age', value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj.age = value

The output that I got by running,

mary = Person('Mary M', 30)
print(vars(mary))

is, an infinite loop of the log in __set__ function,

INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30
INFO:root:Updating 'age' to 30

It appears from the output, that we cannot have same name, for attribute of the object in class and the attribute of the same object in descriptor. Why so?

Also, is it related to instance of descriptor having the same name?


Solution

  • Because obj.age = whatever invokes the descriptor protocol, which will eventually call __set__!

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj.age = value # this calls `Logging.__set__` recursively