CAP 12 · LEC 06·Python avanzado

Descriptores: __get__, __set__ y atributos controlados

Los descriptores son el mecanismo que hace posible @property, classmethod y staticmethod. Implementar __get__ y __set__ te da control total sobre cómo se accede y almacena un atributo.

● AVANZADO11 min lectura3 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

El protocolo descriptor

Un descriptor es cualquier objeto que define al menos uno de: __get__, __set__ o __delete__. Cuando se asigna como atributo de clase, Python lo invoca automáticamente al acceder al atributo desde una instancia.

# Un descriptor básico: loggea cada acceso class LoggedAttribute: """Descriptor que registra lecturas y escrituras.""" def __set_name__(self, owner, name): # Llamado cuando se asigna el descriptor a una clase self.name = name self.private_name = f"_{name}" def __get__(self, instance, owner): if instance is None: return self # acceso desde la clase (MyClass.attr) value = getattr(instance, self.private_name, None) print(f"GET {self.name} = {value!r}") return value def __set__(self, instance, value): print(f"SET {self.name} = {value!r}") setattr(instance, self.private_name, value) def __delete__(self, instance): print(f"DEL {self.name}") delattr(instance, self.private_name) class User: name = LoggedAttribute() # el descriptor se asigna a la CLASE email = LoggedAttribute() u = User() u.name = "Ana" # SET name = 'Ana' u.email = "ana@x.com" # SET email = 'ana@x.com' print(u.name) # GET name = 'Ana' → Ana # El descriptor se comparte: existe UNA instancia del descriptor por clase # pero los datos se almacenan en CADA instancia (en _name, _email) u2 = User() u2.name = "Carlos" # SET name = 'Carlos' print(u.name) # GET name = 'Ana' — no se mezclan
SalidaSET name = 'Ana' SET email = 'ana@x.com' GET name = 'Ana' Ana SET name = 'Carlos' GET name = 'Ana' Ana

Descriptor de datos vs no-datos

La distinción entre descriptores de datos y no-datos determina la prioridad frente a los atributos de instancia: los de datos tienen mayor prioridad, los no-datos pueden ser "tapados" por atributos de instancia.

# Descriptor de DATOS: tiene __set__ (y/o __delete__) # → tiene prioridad sobre __dict__ de la instancia class DataDescriptor: def __get__(self, obj, objtype=None): return obj.__dict__.get("_x", 0) def __set__(self, obj, value): obj.__dict__["_x"] = value # Descriptor de NO-DATOS: solo tiene __get__ # → la instancia puede "taparlo" con su propio atributo class NonDataDescriptor: def __get__(self, obj, objtype=None): if obj is None: return self return "valor del descriptor" class MyClass: data_attr = DataDescriptor() non_data_attr = NonDataDescriptor() obj = MyClass() # Descriptor de datos: siempre gana obj.__dict__["data_attr"] = "intento de override" print(obj.data_attr) # 0 — el descriptor gana # Descriptor de no-datos: la instancia puede taparlo obj.__dict__["non_data_attr"] = "override de instancia" print(obj.non_data_attr) # "override de instancia" — la instancia gana # @property es un descriptor de datos (tiene __set__ aunque sea solo-lectura) # @staticmethod y @classmethod son descriptores de no-datos print(type(MyClass.__dict__["data_attr"])) # DataDescriptor print(type(MyClass.__dict__["non_data_attr"])) # NonDataDescriptor
Salida0 override de instancia <class '__main__.DataDescriptor'> <class '__main__.NonDataDescriptor'>
@property es un descriptor

@property crea un objeto property que implementa __get__, __set__ y __delete__. Cuando haces obj.attr, Python ve que attr en la clase es un descriptor con __get__ y lo llama. Eso es todo el "magic" de @property.

Implementar un validador reutilizable

El poder real de los descriptores es la reutilización: un solo descriptor de validación puede aplicarse a decenas de atributos en cualquier clase, sin duplicar código.

class Validated: """Descriptor base para validación reutilizable.""" def __set_name__(self, owner, name): self.name = name self.storage_name = f"_{name}" def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self.storage_name, None) def __set__(self, instance, value): value = self.validate(value) # template method setattr(instance, self.storage_name, value) def validate(self, value): raise NotImplementedError class NonNegative(Validated): def validate(self, value): if not isinstance(value, (int, float)): raise TypeError(f"{self.name} debe ser numérico, recibido {type(value).__name__}") if value < 0: raise ValueError(f"{self.name} debe ser ≥ 0, recibido {value}") return value class MaxLength(Validated): def __init__(self, max_len: int): self.max_len = max_len def validate(self, value: str) -> str: if len(value) > self.max_len: raise ValueError(f"{self.name} demasiado largo: {len(value)} > {self.max_len}") return value class Product: price = NonNegative() stock = NonNegative() name = MaxLength(50) def __init__(self, name: str, price: float, stock: int): self.name = name self.price = price self.stock = stock p = Product("Python Handbook", 29.99, 100) print(p.price) # 29.99 print(p.stock) # 100 try: p.price = -5 # ValueError: price debe ser ≥ 0, recibido -5 except ValueError as e: print(e) try: p.name = "A" * 60 # ValueError: name demasiado largo except ValueError as e: print(e)
Salida29.99 100 price debe ser ≥ 0, recibido -5 name demasiado largo: 60 > 50

Cómo funciona @property internamente

property es una clase nativa que implementa el protocolo descriptor. Entender su implementación muestra cómo funciona @property, @attr.setter y @attr.deleter realmente.

# @property es azúcar para: # temperature = property(fget=get_temp, fset=set_temp) class Temperature: def __init__(self, celsius: float): self._celsius = celsius # Versión explicita (sin @) def _get_celsius(self): return self._celsius def _set_celsius(self, value: float): if value < -273.15: raise ValueError("Temperatura bajo el cero absoluto") self._celsius = value celsius = property(_get_celsius, _set_celsius) # Propiedad derivada (calculada, sin setter) @property def fahrenheit(self) -> float: return self._celsius * 9/5 + 32 @fahrenheit.setter def fahrenheit(self, f: float): self.celsius = (f - 32) * 5/9 t = Temperature(100.0) print(t.celsius) # 100.0 print(t.fahrenheit) # 212.0 t.fahrenheit = 32.0 print(t.celsius) # 0.0 # Inspeccionar: property es un descriptor print(type(Temperature.__dict__["celsius"])) # <class 'property'> print(type(Temperature.__dict__["fahrenheit"])) # <class 'property'> # property.__get__, property.__set__ existen p = Temperature.__dict__["fahrenheit"] print(p.fget) # <function Temperature.fahrenheit at 0x...> print(p.fset) # <function Temperature.fahrenheit at 0x...>
Salida100.0 212.0 0.0 <class 'property'> <class 'property'>

Practica