Definición y funcionamiento de descriptores
Los descriptores en Python son un mecanismo avanzado que permite personalizar el acceso a atributos de los objetos. Un descriptor se define como una clase que implementa al menos uno de los métodos especiales: __get__, __set__ o __delete__. Estos métodos se denominan en conjunto el protocolo de descriptores.
def __get__(self, obj, objtype=None):
"""Se ejecuta al acceder al atributo"""
pass
def __set__(self, obj, value):
"""Se ejecuta al asignar un valor al atributo"""
pass
def __delete__(self, obj):
"""Se ejecuta al eliminar el atributo con del"""
pass
</div>Los descriptores deben definirse como atributos de clase, no dentro del `__init__`. Son responsables de interceptar las operaciones de acceso, asignación y eliminación de atributso en la clase que los utiliza.
Clasificación de descriptores
-----------------------------
Existen dos categorías principales de descriptores:
- **Descriptores de datos**: Implementan tanto `__get__` como `__set__` (y opcionalmente `__delete__`)
- **Descriptores no-datos**: Solo implementan `__get__`
<div>```
# Descriptor de datos
class ValidadorEntero:
def __get__(self, obj, objtype=None):
return getattr(obj, '_edad', None)
def __set__(self, obj, valor):
if not isinstance(valor, int):
raise TypeError("Se requiere un entero")
obj._edad = valor
# Descriptor no-datos
class SoloLectura:
def __get__(self, obj, objtype=None):
return "Valor inmutable"
El sistema de resolución de atributos en Python sigue un orden específico:
- Atributos de clase
- Descriptores de datos
- Atributos de instancia
- Descriptores no-datos
- Método
__getattr__(si existe)
def __init__(self):
self.descriptor = 42 # Intento de sobrescritura
ej = EjemploPrioridad() print(ej.descriptor) # Ejecuta el descriptor, no accede a la instancia
</div>Implementación práctica: Sistema de validación de tipos
-------------------------------------------------------
Los descriptores son ideales para crear sistemas de validación de atributos:
<div>```
class CampoValidado:
def __init__(self, nombre, tipo_esperado):
self.nombre = nombre
self.tipo_esperado = tipo_esperado
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.nombre)
def __set__(self, obj, valor):
if not isinstance(valor, self.tipo_esperado):
raise ValueError(f"Se esperaba {self.tipo_esperado.__name__}")
obj.__dict__[self.nombre] = valor
class Producto:
nombre = CampoValidado('nombre', str)
precio = CampoValidado('precio', float)
cantidad = CampoValidado('cantidad', int)
def __init__(self, nombre, precio, cantidad):
self.nombre = nombre
self.precio = precio
self.cantidad = cantidad
# Uso del sistema
articulo = Producto("Laptop", 999.99, 5)
articulo.precio = 1299.99 # Válido
# articulo.precio = "caro" # Lanza ValueError
Los descriptores permiten implementar decoradores avanzados:
def __get__(self, obj, objtype=None):
def wrapper(*args, **kwargs):
print(f"Llamando a {self.metodo.__name__} como método de clase")
return self.metodo(objtype, *args, **kwargs)
return wrapper
class MetodoEstatico: def init(self, metodo): self.metodo = metodo
def __get__(self, obj, objtype=None):
def wrapper(*args, **kwargs):
print(f"Llamando a {self.metodo.__name__} como método estático")
return self.metodo(*args, **kwargs)
return wrapper
class Calculadora: @MetodoClase def operacion_clase(cls, x, y): return x + y
@MetodoEstatico
def operacion_estatica(x, y):
return x * y
calc = Calculadora() Calculadora.operacion_clase(2, 3) # Ejecuta wrapper Calculadora.operacion_estatica(4, 5) # Ejecuta wrapper
</div>Propiedades con cálculo diferido
--------------------------------
Los descriptores permiten implementar propiedades que calculan su valor solo cuando se accede por primera vez:
<div>```
class PropiedadDiferida:
def __init__(self, funcion):
self.funcion = funcion
self.nombre = funcion.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
valor = self.funcion(obj)
setattr(obj, self.nombre, valor) # Cache en la instancia
return valor
class Circulo:
def __init__(self, radio):
self.radio = radio
@PropiedadDiferida
def area(self):
print("Calculando área...")
return 3.1416 * self.radio ** 2
@PropiedadDiferida
def perimetro(self):
print("Calculando perímetro...")
return 2 * 3.1416 * self.radio
c = Circulo(5)
print(c.area) # Calcula y almacena
print(c.area) # Devuelve valor almacenado sin recalcular
Al trabajar con descriptores, es crucial entender que:
- Los descriptores deben ser instancias de clase, no atributos de instancia
- La prioridad entre descriptores y atributos determina el comportamiento
- Los descriptores de datos tienen mayor prioridad que los atributos de instancia
- Los descriptores no-datos tienen menor prioridad que los atributos de instancia
def __set__(self, obj, valor):
print(f"Descriptor intercepta asignación: {valor}")
class Ejemplo: atributo = DescriptorConPrioridad()
instancia = Ejemplo() instancia.atributo = 42 # Ejecuta set del descriptor print(instancia.atributo) # Ejecuta get del descriptor
</div>Los descriptores son la base para muchas características fundamentales de Python, incluyendo métodos, propiedades, slots y decoradores. Su correcta implementación permite crear APIs más expresivas y sistemas de validación robustos.