Los descriptores en Python son el mecanismo subyacente que hace posibles property, classmethod y staticmethod. Todo acceso a un atributo de clase pasa antes por el protocolo descriptor: si el objeto encontrado en la clase define __get__, Python lo llama en lugar de devolver el objeto directamente. Entender esto te permite crear atributos con validación automática, campos de ORM propios y proxies transparentes.
El protocolo descriptor
Un descriptor es cualquier objeto que implemente al menos uno de estos métodos:
__get__(self, obj, objtype=None) se invoca al leer el atributo__set__(self, obj, value) se invoca al asignar__delete__(self, obj) se invoca al ejecutardel
Si el descriptor define tanto __get__ como __set__ (o __delete__), se llama data descriptor y tiene prioridad sobre el __dict__ de la instancia. Si solo define __get__, es un non-data descriptor y la instancia puede sobreescribirlo con una clave en su __dict__.
# Non-data descriptor: solo __get__
class MiDescriptor:
def __get__(self, obj, objtype=None):
if obj is None:
return self # acceso desde la clase
return 42
class MiClase:
valor = MiDescriptor()
mc = MiClase()
print(mc.valor) # 42 llama a __get__
mc.__dict__['valor'] = 99
print(mc.valor) # 99 el __dict__ tiene prioridad en non-data
Data descriptor: prioridad sobre __dict__
class DataDescriptor:
def __set_name__(self, owner, name):
self.nombre_privado = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.nombre_privado, None)
def __set__(self, obj, value):
setattr(obj, self.nombre_privado, value)
class Punto:
x = DataDescriptor()
y = DataDescriptor()
p = Punto()
p.x = 10
p.y = 20
print(p.x, p.y) # 10 20
print(p.__dict__) # {'_x': 10, '_y': 20}
__set_name__ (disponible desde Python 3.6) se llama automáticamente cuando la clase se define, pasando el nombre del atributo. Es la forma correcta de que un descriptor sepa cómo se llama sin necesidad de pasárselo manualmente.
Implementar property desde cero
El builtin property es sencillamente un data descriptor. Aquí tienes una implementación mínima fiel al original:
class miproperty:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc or (fget.__doc__ if fget else None)
def __set_name__(self, owner, name):
self.atrib = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"'{self.atrib}' no tiene getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"'{self.atrib}' es de solo lectura")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"'{self.atrib}' no se puede borrar")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
class Temperatura:
def __init__(self, celsius: float):
self._celsius = celsius
@miproperty
def celsius(self) -> float:
"""Temperatura en grados Celsius."""
return self._celsius
@celsius.setter
def celsius(self, valor: float):
if valor < -273.15:
raise ValueError(f"Temperatura {valor} por debajo del cero absoluto")
self._celsius = valor
@miproperty
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperatura(100)
print(t.fahrenheit) # 212.0
t.celsius = -10
print(t.fahrenheit) # 14.0
Descriptor de validación reutilizable
Uno de los usos más prácticos de los descriptores es crear campos tipados con validación que se reutilizan en varias clases sin repetir código:
class CampoEntero:
def __set_name__(self, owner, name):
self.nombre = name
self.nombre_privado = f'_campo_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.nombre_privado, 0)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError(
f"'{self.nombre}' requiere int, recibido {type(value).__name__}"
)
if value < 0:
raise ValueError(f"'{self.nombre}' debe ser >= 0, recibido {value}")
setattr(obj, self.nombre_privado, value)
class Producto:
stock = CampoEntero()
precio_centimos = CampoEntero()
def __init__(self, nombre: str, stock: int, precio_centimos: int):
self.nombre = nombre
self.stock = stock
self.precio_centimos = precio_centimos
@property
def precio(self) -> float:
return self.precio_centimos / 100
p = Producto("Libro Python", 50, 2999)
print(p.precio) # 29.99
p.stock = -1 # ValueError: 'stock' debe ser >= 0, recibido -1
p.stock = "muchos" # TypeError: 'stock' requiere int, recibido str
Descriptores en el contexto de clases: objtype
Cuando accedes al descriptor desde la clase (no desde una instancia), obj es None y objtype es la propia clase. Esto permite implementar comportamientos distintos según el contexto:
class Registrador:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
# Acceso desde la clase: devuelvo info del descriptor
return f"Descriptor '{self.name}' en {objtype.__name__}"
return getattr(obj, f'_{self.name}', None)
def __set__(self, obj, value):
print(f"[LOG] Asignando {self.name} = {value!r}")
setattr(obj, f'_{self.name}', value)
class Usuario:
email = Registrador()
nombre = Registrador()
print(Usuario.email) # Descriptor 'email' en Usuario
u = Usuario()
u.email = "[email protected]" # [LOG] Asignando email = '[email protected]'
print(u.email) # [email protected]
Cuándo usar descriptores vs property
Usa property cuando la lógica es específica de una clase concreta. Opta por un descriptor cuando el mismo comportamiento de validación o transformación se repite en varias clases, como ocurre en ORMs (Django Models, SQLAlchemy), frameworks de formularios o sistemas de configuración tipada.
Los descriptores son la base del sistema de atributos de Python. Comprender su funcionamiento hace que herramientas como functools.cached_property, los métodos de instancia, los métodos de clase y los métodos estáticos dejen de ser magia para convertirse en patrones claros y reproducibles.
