Ir al contenido principal

Tutorial de descriptores en Python

Descubre qué son los descriptores en Python, cuándo deberías usarlos y por qué te convienen.
Actualizado 17 sept 2026  · 13 min leer

Explorar con IA

ChatGPTClaudePerplexity

Los descriptores de Python, o descriptores en general, te ofrecen una técnica potente para escribir código reutilizable que se pueda compartir entre clases. Pueden parecerse a la herencia, pero técnicamente no lo son. Son un mecanismo de uso general para interceptar el acceso a atributos. De hecho, los descriptores están detrás de propiedades, métodos estáticos, métodos de clase, super(), etc.

Los descriptores se incorporaron en Python 2.2 y, desde entonces, se consideran como una característica “mágica” que dio un nuevo estilo a las clases tradicionales. Son clases que te permiten gestionar propiedades en otra clase. En concreto, implementan la interfaz de los métodos __get__(), __set__() y __delete__(), lo que los hace interesantes por muchas razones. Por ejemplo, son la base de decoradores de clase y de propiedad que ya habrás visto en Python.

En pocas palabras, una clase que implementa los métodos __get__(), __set()__ o __delete()__ para un objeto se conoce como "descriptor". Citando la documentación oficial de Python, un descriptor es un atributo de objeto con comportamiento de enlace (binding), cuyo acceso de atributo ha sido redefinido por los métodos del protocolo de descriptores. Esos métodos son __get__(), __set__() y __delete__() (Fuente).

El comportamiento de enlace en el contexto de los descriptores significa que se controla cómo se puede establecer, consultar (get) o eliminar el valor de una variable, objeto o conjunto de datos. Toda esta interacción queda vinculada a ese dato concreto y solo se aplica al dato sobre el que lo definas.

Los descriptores pueden clasificarse en data descriptors y non-data descriptors. Si el descriptor que escribes solo tiene el método __get__(), es un non-data descriptor; en cambio, si implementa __set__() y __delete__(), se llama data descriptor. Los non-data descriptors son solo de lectura, mientras que los data descriptors permiten lectura y escritura.

Es importante tener en cuenta que los descriptores se asignan a una clase, no a una instancia. Al modificar la clase, sobrescribes o eliminas el propio descriptor, en lugar de ejecutar su código (IBM Developer).

Por último, la clase descriptor no se limita a estos tres métodos; puede contener cualquier otro atributo además de los métodos get, set y delete.

Veamos con más detalle los métodos get, set y delete, inspirándonos en esta página de IBM Developer:

  • self es la instancia del descriptor que creas (Real Python).
  • object es la instancia del objeto al que está enlazado tu descriptor (Real Python).
  • type es el tipo del objeto al que está enlazado el descriptor (Real Python).
  • value es el valor que se asigna al atributo del descriptor. get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() accede al atributo cuando quieres extraer información. Devuelve el valor del atributo o lanza la excepción AttributeError si el atributo solicitado no existe.

  • __set__() se llama al asignar un valor a un atributo. No devuelve nada, pero puede lanzar AttributeError.

  • __delete__() controla una operación de borrado, es decir, cuando quieres eliminar el atributo de un objeto. No devuelve nada.

¡Ahora veamos el propósito de los descriptores con algunos ejemplos!

Propósito de los descriptores

Definamos una clase car con tres atributos: make, model y fuel_cap. Usarás el método __init__() para inicializarlos. Después, utilizarás la función mágica __str__(), que simplemente devolverá un texto con los tres atributos que pases al crear el objeto.

Ten en cuenta que el método __str__() devuelve la representación en cadena del objeto. Se llama cuando ejecutas print() o la función str() sobre un objeto de la clase.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

Como ves en la salida, todo parece correcto.

Ahora cambiemos la capacidad del depósito a menos 40.

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

Un momento, algo no cuadra, ¿verdad? La capacidad del depósito no puede ser negativa. Sin embargo, Python lo acepta sin error. Esto se debe a que Python es un lenguaje dinámico que no aplica comprobación de tipos de forma explícita.

Para evitarlo, añadamos una condición if en __init__() que verifique si la capacidad es válida. Si no lo es, lanzamos una excepción ValueError.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

Como puedes ver, ahora el programa lanza ValueError si la capacidad es menor que cero.

Sin embargo, puede surgir otro problema: ¿y si fuel_cap es un float o una cadena? Y no solo fuel_cap; también make y model podrían ser enteros. En todos estos casos, el programa no lanzaría excepción.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Para gestionar esto, podrías pensar en añadir otra condición if o usar isinstance para la comprobación de tipos.

Usemos ahora la función integrada isinstance para controlar el error.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

¡Genial! También has resuelto este caso.

Pero ¿y si más tarde cambias explícitamente el atributo fuel_cap a -40? No funcionará, porque la comprobación de tipos solo se hace una vez en __init__(). Como sabes, __init__() es el constructor y solo se ejecuta al crear el objeto. Por tanto, tu comprobación personalizada fallará después.

Veámoslo con un ejemplo.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

¡Ahí lo tienes! Has podido saltarte la comprobación de tipos.

Ahora piensa que, además, tienes más atributos del coche como consumo, precio, accesorios, etc., que también requieren validación, y quizá algunos deberían ser solo de lectura. ¿No sería un lío?

Para resolver todo esto, Python tiene los descriptores.

Como ya viste, cualquier clase que implemente los métodos mágicos __get__(), __set()__ o __delete()__ del protocolo de descriptores se considera un descriptor. También te dan control adicional sobre cómo debe funcionar un atributo, por ejemplo, si debe ser de solo lectura o permitir escritura.

Extendamos ahora el ejemplo anterior añadiendo los métodos de descriptor de Python.

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

Si la clase Descriptor te resulta críptica, no te preocupes. Desgranémosla:

  • El método __init__() de Descriptor inicializa la variable local __fuel_cap a cero. El dunder (doble guion bajo) al principio indica que la variable es privada. Usamos dunder para distinguir el atributo de capacidad de combustible de la clase Descriptor del de la clase Car.
  • Como ya sabes, __get__() recupera el atributo y devuelve la capacidad de combustible. Recibe tres argumentos: el objeto descriptor, la instancia de la clase que contiene el descriptor (por ejemplo, car2) y el owner, que es la clase a la que pertenece la instancia, es decir, Car. En este método simplemente devuelves el valor del atributo fuel_cap, que se establece en __set__().
  • El método __set__() se invoca cuando se asigna un valor al atributo y, a diferencia de __get__(), no devuelve nada. Recibe la instancia (igual que en __get__()) y el valor que asignas. Aquí compruebas que el valor para fuel_cap sea un entero; si no, lanzas TypeError. Luego verificas que no sea menor que cero; si lo es, lanzas ValueError. Tras las comprobaciones, actualizas fuel_cap con el valor.
  • Por último, __delete__() se llama al borrar el atributo del objeto y, como __set__(), no devuelve nada.

La clase Car permanece igual salvo por añadir la instancia de fuel_cap de la clase Descriptor(). Recuerda: la instancia del descriptor debe añadirse a la clase como atributo de clase, no como atributo de instancia.

En cuanto asignas la variable local fuel_cap en __init__(), se invoca el método __set__() del descriptor.

Cambiemos ahora la capacidad a un valor negativo para comprobar si se lanza ValueError.

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

Si recuerdas aquí, la comprobación de tipos fallaba cuando cambiabas el valor después, ya que solo se hacía en __init__(). Probemos ahora a actualizar fuel_cap con un valor de cadena y veamos si da error.

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

¡Perfecto! Como ves, también funciona cuando actualizas el atributo más tarde.

Hay, eso sí, un pequeño inconveniente: cuando creas una nueva instancia de la clase, el valor anterior puede verse sobrescrito. La razón es que los descriptores están asociados a la clase y no a la instancia.

Veámoslo con este ejemplo.

car3 = Car("BMW","X7",48) #creada una nueva instancia 'car3' con valores distintos
48

Si imprimes la instancia car2, verás que sus valores se han sobrescrito con los de car3.

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

Conclusión

Enhorabuena por completar el tutorial.

Este tutorial está pensado para quienes ya se sienten cómodos con Python y quieren dominar conceptos avanzados. Como buen ejercicio, puedes investigar cómo resolver el problema de sobrescritura entre instancias que hemos visto hoy.

No dudes en dejar tus preguntas sobre este tutorial en los comentarios.

Si quieres seguir aprendiendo Python, echa un vistazo al itinerario de aprendizaje Python Programming de DataCamp.

Consulta también nuestro tutorial de funciones en Python.

Temas
Python

Cursos de Python

Curso

Introducción a Python

4 h
7M
Domina los fundamentos del análisis de datos con Python en cuatro horas y descubre sus paquetes más usados.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado

Tutorial

Tutorial de iteradores y generadores en Python

Explora la diferencia entre iteradores y generadores en Python y descubre cuál conviene usar en cada situación.
Kurtis Pykes 's photo

Kurtis Pykes

10 min

Tutorial

Funciones en Python: cómo llamar y escribir funciones

Descubre cómo escribir funciones en Python reutilizables y eficientes. Domina los parámetros, las sentencias return y temas avanzados como las funciones lambda. Organiza mejor tu código con main() y otras buenas prácticas.
Karlijn Willems's photo

Karlijn Willems

14 min

Tutorial

Tutorial de Python sobre conjuntos y teoría de conjuntos

Aprende sobre los conjuntos en Python: qué son, cómo crearlos, cuándo usarlos, funciones incorporadas y su relación con las operaciones de la teoría de conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

Tutorial

Tutorial Python Docstrings : Ejemplos y Formato de Cadenas Doc Pydoc, Numpy, Sphinx

Aprende sobre las Docstrings de Python. Encuentra diferentes ejemplos y tipos de formato de docstrings para Sphinx, Numpy y Pydoc.
Aditya Sharma's photo

Aditya Sharma

15 min

Tutorial

Tutorial de módulos de Python: Importarlos, escribirlos y utilizarlos

Aprende a crear e importar módulos de Python. ¡Descubre las mejores prácticas, ejemplos y consejos para escribir código Python reutilizable, organizado y eficiente!

Nishant Kumar

8 min

Tutorial

Tutorial sobre cómo concatenar cadenas en Python

Aprende varios métodos para concatenar cadenas en Python, con ejemplos que ilustran cada técnica.
DataCamp Team's photo

DataCamp Team

5 min

Ver MásVer Más