0

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.

  • 1
    For me the code falls with a `RecursionError` which is expected because `setattr(obj,self.name,valor)` calls the `__set__` method of `Descriptor`. – Michael Butscher Jun 17 '23 at 09:49
  • Could you elaborate as to why setattr calls `__set__` again? – Jaime Yepes de Paz Jun 17 '23 at 10:20
  • 2
    (1) `setattr(obj, 'nombre', valor)` does the same as (2) `obj.nombre = valor` would. Because `obj.nombre` is a descriptor, both (1) and (2) result in the descriptor's `__set__` method being called. – slothrop Jun 17 '23 at 10:23

1 Answers1

1

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.

slothrop
  • 3,218
  • 1
  • 18
  • 11
  • I believe you are saying that `__init__` in Prueba is calling `__set__` because it's setting the attribute `set` (which seems not to be the set instance I declared before??). Then, I call self.name expecting to read the attribute easily, but instead, since I have a descriptor called `name` inside the class, I go to `__get__` and execute ``getattr``. Am I right? If so, I still don't understand what the exact problem with getattr is. Is it that, inside it, it's calling self.name again, thus initializing ``__get__`` another time? – Jaime Yepes de Paz Jun 17 '23 at 09:59
  • 1
    I edited the answer to be clearer about this. The core problem is that the value of the object's `nombre` attribute is the descriptor object. So we need to find another attribute name (for example `_nombre`) in which to store the value itself. – slothrop Jun 17 '23 at 10:07
  • So doing setattr(object, attribute, value) is the same as object.attribute=value, which is exactly what I'd called before, right? It's a bit weird because I think that some names are dummy when they aren't. For example, in the `__init__`, where I use `nombre` for both an input variable and the descriptor itself, how does it know which is which? Sorry for these questions, but I'm new to Python (and programming in general except some Matlab) and everything is a rabbit hole. – Jaime Yepes de Paz Jun 17 '23 at 10:24
  • *how does it know which is which?* In that case, it's because the variable in the function's local scope (`nombre`) is separate from the attribute in the object's namespace (`self.nombre`). That doesn't cause problems and is a common Python idiom, especially in constructors. – slothrop Jun 17 '23 at 10:26
  • Okay, I think I got it. So to recap, I have defined an _attribute_ which is of type *descriptor*, just as I could have assigned it a string, int or whatever, and I access it with obj.atr. (I think many other objects have or are descriptors, but I mean a customised one). So the effect this has is that when doing certain operations such as reading, assigning or deleting, instead of behaving 'normally' like in regular python variables, it does exactly what I programmed in its ``get`` and ``set`` methods. And for these custom descriptors to be generated, I must always instantiate them first. – Jaime Yepes de Paz Jun 17 '23 at 10:36
  • 1
    Sounds like you got it! One thing to add - if you haven't seen this already, take a look at **properties** (https://stackoverflow.com/questions/6304040/real-world-example-about-how-to-use-property-feature-in-python). These allow you to create the kind of descriptor that you were using here, but without writing so much explicit code. – slothrop Jun 17 '23 at 11:54