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.
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
:
In __init__
, executing self.nombre = nombre
invokes the descriptor's __set__
method (as expected)
That __set__
method invokes setattr(obj, 'nombre', valor)
(because the descriptor's self.name
is 'nombre'
)
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
.