Descriptors en Python: guía práctica

Actualizado el: 22/07/2026
Tempo de leitura: 7 minutos
Código Python y atributos

Los descriptors en Python son uno de los mecanismos que hacen posibles propiedades, métodos, validaciones, ORMs y muchas funciones de frameworks. Aunque al principio parecen complejos, la idea central es sencilla: un objeto almacenado en una clase puede controlar qué ocurre cuando otro atributo se lee, se modifica o se elimina. En esta guía aprenderás el protocolo descriptor, crearás ejemplos reutilizables con __get__, __set__, __delete__ y __set_name__, y entenderás cuándo conviene usarlo.

Como se trata de una característica avanzada orientada a objetos, resulta útil dominar clases, instancias y funciones. También puedes repasar decoradores en Python, dataclasses en Python, type hints en Python y herencia múltiple y MRO.

¿Qué es un descriptor en Python?

Un descriptor es un objeto que implementa al menos uno de los métodos especiales __get__, __set__ o __delete__. Cuando ese objeto se asigna como atributo de clase, Python llama automáticamente a esos métodos durante el acceso al atributo. Esto permite centralizar validación, conversión, registro de eventos, carga diferida, control de acceso y valores calculados.

Este comportamiento forma parte del modelo de datos oficial. La sección de descriptors del modelo de datos explica las reglas de búsqueda, mientras que la guía oficial de descriptors muestra cómo funciones, métodos y propiedades utilizan el mismo protocolo.

Primer descriptor con __get__

El descriptor más sencillo puede definir únicamente __get__. El siguiente ejemplo devuelve un saludo cuando se accede desde una instancia y devuelve el propio descriptor cuando se accede desde la clase.

class Saludo:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return f"Hola, {instance.nombre}!"


class Usuario:
    mensaje = Saludo()

    def __init__(self, nombre):
        self.nombre = nombre


usuario = Usuario("Ana")
print(usuario.mensaje)
print(Usuario.mensaje)

El argumento instance representa el objeto que solicitó el atributo. Recibe None cuando el acceso se realiza directamente desde la clase. El argumento owner contiene la clase propietaria. Devolver self en el acceso de clase facilita la inspección y la configuración.

Validación con __set__

La validación es uno de los usos más prácticos. El siguiente descriptor acepta únicamente enteros no negativos y almacena el valor en cada instancia.

class EnteroPositivo:
    def __set_name__(self, owner, name):
        self.nombre_publico = name
        self.nombre_privado = f"_{name}"

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.nombre_privado)

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError(
                f"{self.nombre_publico} debe ser entero"
            )
        if value < 0:
            raise ValueError(
                f"{self.nombre_publico} no puede ser negativo"
            )
        setattr(instance, self.nombre_privado, value)


class Persona:
    edad = EnteroPositivo()

    def __init__(self, edad):
        self.edad = edad

El método __set_name__ se ejecuta cuando Python crea la clase propietaria. Informa al descriptor el nombre del atributo asignado, lo que permite reutilizar la misma clase descriptor en muchos campos sin configurar cada nombre manualmente.

Data y non-data descriptors

Los descriptors suelen dividirse en dos grupos. Un data descriptor implementa __set__ o __delete__, y también puede implementar __get__. Un non-data descriptor implementa solo __get__. La diferencia importa porque Python les concede distinta prioridad durante la búsqueda de atributos.

  • Los data descriptors tienen prioridad sobre los valores del diccionario de la instancia.
  • Los non-data descriptors pueden ser ocultados por un atributo de instancia con el mismo nombre.
  • Los métodos normales son non-data descriptors.
  • Los objetos property son data descriptors.

Esta prioridad explica por qué escribir directamente en instance.__dict__ no siempre cambia el resultado de un atributo. También conecta los descriptors con la herencia y el orden de resolución de métodos.

Cómo property utiliza descriptors

El decorador @property crea un objeto descriptor. Ese objeto guarda funciones getter, setter y deleter, y las llama cuando el atributo se utiliza. En muchas clases, una propiedad es la solución más clara porque el comportamiento pertenece a un campo concreto.

class Producto:
    def __init__(self, precio):
        self.precio = precio

    @property
    def precio(self):
        return self._precio

    @precio.setter
    def precio(self, value):
        if value < 0:
            raise ValueError("El precio no puede ser negativo")
        self._precio = float(value)

Elige una propiedad cuando la regla se usa en una sola clase. Elige un descriptor personalizado cuando el mismo comportamiento debe reutilizarse en varios campos o clases. Esta comparación evita abstracciones innecesarias.

Descriptor reutilizable para textos

El siguiente descriptor valida textos obligatorios, elimina espacios laterales y aplica una longitud máxima.

class TextoObligatorio:
    def __init__(self, maximo=100):
        self.maximo = maximo

    def __set_name__(self, owner, name):
        self.nombre = name
        self.nombre_almacenamiento = f"_{name}"

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.nombre_almacenamiento)

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.nombre} debe ser texto")

        value = value.strip()
        if not value:
            raise ValueError(f"{self.nombre} es obligatorio")
        if len(value) > self.maximo:
            raise ValueError(
                f"{self.nombre} supera {self.maximo} caracteres"
            )

        setattr(instance, self.nombre_almacenamiento, value)


class Articulo:
    titulo = TextoObligatorio(80)
    autor = TextoObligatorio(50)

    def __init__(self, titulo, autor):
        self.titulo = titulo
        self.autor = autor

Este patrón reduce repetición y mantiene las clases de dominio más limpias. Aun así, los mensajes de error deben ser específicos, porque una excepción genérica dentro de un acceso automático resulta difícil de diagnosticar.

Guardar valores por instancia

Un error frecuente consiste en guardar el valor actual dentro del descriptor, por ejemplo con self.value. El descriptor pertenece a la clase y se comparte entre todas las instancias, por lo que distintos objetos terminarían compartiendo datos. Los valores deben almacenarse en la instancia, normalmente bajo un nombre privado.

Otra opción es weakref.WeakKeyDictionary, que asocia instancias y valores sin impedir que las instancias se liberen de memoria. Puede ayudar cuando el objeto propietario no permite atributos adicionales, pero introduce más complejidad.

Evitar recursión infinita

Dentro de __set__, volver a asignar el atributo público activa otra vez el descriptor. Por ejemplo, ejecutar instance.edad = value dentro del descriptor de edad llama a __set__ indefinidamente. Utiliza un nombre de almacenamiento distinto, como _edad, escribe de forma controlada en __dict__ o emplea otra estrategia segura.

La misma precaución se aplica a __get__. Leer el nombre público desde su propio descriptor repite la búsqueda. Una convención clara de nombres privados evita este problema.

Descriptors y herencia

Los descriptors participan en la herencia normal de clases. Una subclase puede heredarlos, sustituirlos o declarar otro atributo con el mismo nombre. Los data descriptors suelen conservar prioridad sobre los valores de instancia, mientras que los non-data descriptors pueden ser ocultados.

Conviene probar explícitamente el comportamiento de subclases, especialmente cuando un framework utiliza descriptors para declarar campos. El MRO, las metaclases y los atributos generados pueden cambiar la clase propietaria recibida por __get__.

Descriptors y dataclasses

Los descriptors pueden combinarse con dataclasses, pero los valores predeterminados y el orden de inicialización requieren atención. Una dataclass puede interpretar el descriptor como valor por defecto, mientras el descriptor espera controlar las asignaciones.

En modelos sencillos, field, una propiedad o la validación en __post_init__ pueden ser más fáciles de mantener. Usa un descriptor cuando el comportamiento de campo se repite; utiliza validación de dataclass cuando una regla depende de varios atributos a la vez.

Pruebas de descriptors

Las pruebas deben cubrir asignaciones válidas, tipos incorrectos, rangos inválidos, acceso desde la clase, eliminación si está soportada e independencia entre instancias.

import pytest


def test_valores_independientes():
    primera = Persona(20)
    segunda = Persona(40)
    assert primera.edad == 20
    assert segunda.edad == 40


def test_edad_negativa():
    with pytest.raises(ValueError):
        Persona(-1)

También es recomendable probar serialización, copia e inspección si el descriptor forma parte de una biblioteca. El acceso a atributos parece simple desde fuera, pero puede ejecutar bastante lógica interna.

Cuándo usar descriptors

  • Validación reutilizable en varias clases.
  • Conversión y normalización automáticas.
  • Cálculo diferido o atributos en caché.
  • Mapeo de campos de bases de datos en ORMs.
  • Auditoría de lecturas y escrituras.
  • Diseño de frameworks y bibliotecas.

No conviene usarlos cuando una función, una validación en el constructor o una propiedad resuelven el problema de manera clara. El comportamiento automático es potente, pero puede sorprender a quien no sabe que el acceso al atributo ejecuta código personalizado.

Errores comunes

  • Guardar valores en el objeto descriptor compartido.
  • Usar internamente el nombre público y provocar recursión.
  • No tratar el acceso de clase cuando instance es None.
  • Crear una abstracción compleja para una regla usada una sola vez.
  • No probar herencia, serialización e inspección.
  • Ocultar operaciones costosas detrás de un atributo aparentemente simple.

Conclusión

Los descriptors en Python ofrecen control reutilizable sobre el acceso a atributos y ayudan a entender cómo funcionan métodos, propiedades y campos de muchos frameworks. El protocolo utiliza pocos métodos especiales, pero un diseño correcto exige almacenamiento por instancia, errores previsibles y conocimiento de la prioridad de búsqueda.

Empieza con un descriptor pequeño, pruébalo con varias instancias y compáralo con una propiedad o una función de validación. Cuando el mismo comportamiento se repite de verdad en distintas clases, un descriptor bien diseñado puede reducir duplicación y mantener reglas consistentes sin complicar la API pública.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo leer archivos gigantes con Python sin bloquear el sistema

    Aprende a leer archivos gigantes con Python usando iteración, bloques, generadores, chunks de Pandas, compresión y puntos de control.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Herencia múltiple en Python: MRO, super() y mixins

    Aprende herencia múltiple en Python con MRO, super(), mixins, problema del diamante, inicializadores cooperativos, composición y pruebas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo crear un ejecutable EXE con icono personalizado en Python

    Crea un EXE de Python con PyInstaller, icono ICO, onefile, windowed, archivos de datos, SPEC, rutas seguras, pruebas y distribución.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Uso do super em Python para resolver problemas de herança
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo usar super() en Python sin errores de herencia

    Aprende a usar super() en Python con __init__, MRO, herencia múltiple, mixins, argumentos cooperativos, pruebas y composición.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Uso de multiprocessing em Python para acelerar scripts
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Multiprocessing en Python: usa varios núcleos correctamente

    Aprende multiprocessing en Python con Process, Pool, Queue, memoria compartida, ProcessPoolExecutor, benchmarks y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criação de geradores eficientes usando yield em Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    yield en Python: crea generadores eficientes

    Aprende yield en Python para crear generadores, procesar archivos grandes, construir pipelines, usar yield from y reducir el consumo de

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026