Search code examples
pythonclassdescriptor

Confusied about Descriptor Behaviour


I'm trying to understand descriptors and wrote a simple example that practically emulates the given one:

class Descriptor:
    
    
    def __set_name__(self,obj,name): 
        self.name=name
        
    def __set__(self,obj,valor): 
        
        if (self.name).title()=='Nombre':
            if type(valor)!=str:
                raise TypeError('No es un string')
            else:
                print(f'Estamos cambiando el atributo nombre de {obj} a {valor}')
                setattr(obj,self.name,valor) 
                
        else: print('Hola')
            
    def __get__(self,obj,owner):
        
        return getattr(obj,self.name)  
    
class Prueba:
    
    nombre=Descriptor()
    apellido=Descriptor()
    print(nombre.__dict__)
    print(apellido.__dict__)
    
    def __init__(self,nombre,apellido):
        self.nombre=nombre
        self.apellido=apellido
            
        
baz=Prueba('foo','bar')

print(baz.__dict__)

Which makes the Jupyter Kernel crash (had never happened before).

If I change every nombre or apellido, I get:

{}
{}
Hola
Hola
{}

So somehow the instances of the Descriptor class are empty. Yet when I make Descriptor print self.name in **set **it works. I'm very confused about how to use them to simplify properties.


Solution

  • The problem here is an infinite regress in the __get__ and __set__ methods of the descriptor. For the sake of example, let's focus just on the descriptor object for the nombre attribute, which has self.name == 'nombre'.

    When you initialise an instance of Prueba:

    1. In __init__, executing self.nombre = nombre invokes the descriptor's __set__ method (as expected)

    2. That __set__ method invokes setattr(obj, 'nombre', valor) (because the descriptor's self.name is 'nombre')

    3. The value of object's nombre attribute is the descriptor itself. So that setattr call invokes __set__ in the descriptor again.

    So steps 2 and 3 repeat cyclically until the recursion depth is exceeded.

    Similarly, a call to __get__ executes getattr(obj, 'nombre'). But obj.nombre which is the descriptor object itself, so the result is another call to the descriptor's __get__, and so on in an infinite cycle.

    The docs show a way that you can programmatically store attribute values using a "private" name that avoids this regress.

    So your example descriptor becomes:

    class Descriptor:
        def __set_name__(self,obj,name): 
            self.name=name
            self.private_name = '_' + name
            
        def __set__(self,obj,valor): 
            if (self.name).title()=='Nombre':
                if type(valor)!=str:
                    raise TypeError('No es un string')
                else:
                    print(f'Estamos cambiando el atributo nombre de {obj} a {valor}')
                    setattr(obj,self.private_name,valor) 
                    
            else: print('Hola')
                
        def __get__(self,obj,owner):
            return getattr(obj,self.private_name)  
    

    With this in place, your example of:

    baz=Prueba('foo','bar')
    print(baz.__dict__)
    

    now gives:

    {}
    {}
    Estamos cambiando el atributo nombre de <__main__.Prueba object at 0x7f04e0220ee0> a foo
    Hola
    {'_nombre': 'foo'}
    

    and print(baz.nombre) prints foo.