Descriptores en Python: Protocolo __get__, __set__ y __delete__

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:

  1. Atributos de clase
  2. Descriptores de datos
  3. Atributos de instancia
  4. Descriptores no-datos
  5. 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.

Etiquetas: Python descriptors protocol data-descriptors non-data-descriptors

Publicado el 6-30 21:52