Método especial __getattribute__()
El método __getattribute__() es una característica especial de las clases en Python que permite controlar el acceso a los atributos de una manera muy flexible.
Características principales:
- Se invoca siempre que se accede a un atributo, a diferencia de
__getattr__()que solo se ejecuta cuando el atributo no se encuentra en la instancia ni en la clase. - Si una clase define tanto
__getattribute__()como__getattr__(), el método__getattr__()solo se ejecutará si__getattribute__()lanza una excepciónAttributeError. - Para evitar recursión infinita al acceder a atributos dentro de
__getattribute__(), se debe llamar al método de la clase padre usandosuper(obj, self).__getattribute__(attr). - Este método solo funciona en clases de nuevo estilo (que heredan de
object).
Ejemplo de __getattr__ tradicional
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def __getattr__(self, elemento):
print('Método __getattr__ ejecutado')
# return self.__dict__[elemento]
p1 = Persona('Carlos')
print(p1.nombre)
p1.apellido # Acceso a atributo inexistente, ejecuta __getattr__
Ejemplo de __getattribute__
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def __getattribute__(self, elemento):
print('Este método siempre se ejecuta')
# Se ejecuta tanto para atributos existentes como inexistentes
p1 = Persona('Carlos')
p1.nombre
p1.apellido
Ambos métodos juntos
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def __getattr__(self, elemento):
print('Ejecutando __getattr__')
def __getattribute__(self, elemento):
print('Ejecutando __getattribute__')
raise AttributeError('Simulando error')
p1 = Persona('Carlos')
p1.nombre
p1.apellido
# Cuando __getattribute__ y __getattr__ coexisten, solo se ejecuta __getattribute__
# a menos que este lance AttributeError
Descriptores en Python
Los descriptores son objetos que permiten personalizar el comportamiento de acceso a atributos. Cuando se accede a un atributo cuyo valor es un descriptor, Python puede reemplazar el comportamiento predeterminado y ejecutar los métodos del descriptor.
Protocolo de descriptores
Un descriptor es cualquier objeto que implementa al menos uno de estos tres métodos:
# Se ejecuta al obtener el valor del atributo
descr.__get__(self, obj, tipo=None) --> valor
# Se ejecuta al establecer el valor del atributo
descr.__set__(self, obj, valor) --> None
# Se ejecuta al eliminar el atributo
descr.__delete__(self, obj) --> None
También se puede expresar como:
__get__(self, instancia, propietario) --> valor
__set__(self, instancia, valor)
__delete__(self, instancia)
Tipos de descriptores
Descriptores de datos: Implementan tanto __get__ como __set__ (y opcionalmente __delete__). Tienen mayor prioridad que los atributos de instancia.
Descriptores no de datos: Solo implementan __get__. Los métodos y funciones regulares son descriptores no de datos.
Reglas de prioridad (de mayor a menor):
- Atributos de clase
- Descriptores de datos
- Atributos de instancia
- Descriptores no de datos
- Método
__getattr__()como último recurso
Ejemplo básico de descriptor
class MiDescriptor:
def __get__(self, instancia, propietario):
print("Método __get__ ejecutado")
return hex(id(instancia))
def __set__(self, instancia, valor):
print("Método __set__ ejecutado:", valor)
def __delete__(self, instancia):
print("Método __delete__ ejecutado")
class Datos:
valor = MiDescriptor()
d = Datos()
d.valor # Llama a __get__
d.valor = 50 # Llama a __set__
del d.valor # Llama a __delete__
Datos.valor # __get__ con instancia = None
Desarrollando un descriptor para validación de tipos
class Validador:
def __init__(self, nombre, tipo_esperado):
self.nombre = nombre
self.tipo_esperado = tipo_esperado
def __get__(self, instancia, propietario):
if instancia is None:
return self
return instancia.__dict__[self.nombre]
def __set__(self, instancia, valor):
if not isinstance(valor, self.tipo_esperado):
raise TypeError(f'Se esperaba {self.tipo_esperado.__name__}')
instancia.__dict__[self.nombre] = valor
def __delete__(self, instancia):
del instancia.__dict__[self.nombre]
class Empleado:
nombre = Validador('nombre', str)
edad = Validador('edad', int)
salario = Validador('salario', float)
def __init__(self, nombre, edad, salario):
self.nombre = nombre
self.edad = edad
self.salario = salario
# Uso correcto
emp1 = Empleado('Ana', 30, 2500.50)
print(emp1.nombre)
# Uso incorrecto - lanza excepción
emp2 = Empleado(123, 25, 3000) # TypeError
Usando decoradores para descriptores automáticamente
def validar_tipos(**kwargs):
def decorador(clase):
for nombre, tipo in kwargs.items():
setattr(clase, nombre, Validador(nombre, tipo))
return clase
return decorador
@validar_tipos(nombre=str, edad=int, salario=float)
class Persona:
def __init__(self, nombre, edad, salario):
self.nombre = nombre
self.edad = edad
self.salario = salario
p1 = Persona('Luis', 28, 1500.75)
El decorador @property
El decorador @property es un descriptor de datos que permite definir métodos que se comportan como atributos.
Uso básico
class Habitacion:
def __init__(self, nombre, ancho, largo):
self.nombre = nombre
self.ancho = ancho
self.largo = largo
@property
def area(self):
return self.ancho * self.largo
r1 = Habitacion('Sala', 5, 4)
print(r1.area) # 20
Property con setter y deleter
class Producto:
def __init__(self):
self._precio = 100
self._descuento = 0.2
@property
def precio_final(self):
return self._precio * (1 - self._descuento)
@precio_final.setter
def precio_final(self, valor):
self._precio = valor
@precio_final.deleter
def precio_final(self):
del self._precio
p = Producto()
print(p.precio_final) # 80.0
p.precio_final = 200
print(p.precio_final) # 160.0
Implementando nuestro propio @property
class PropiedadDiferida:
def __init__(self, func):
self.func = func
def __get__(self, instancia, propietario):
if instancia is None:
return self
valor = self.func(instancia)
setattr(instancia, self.func.__name__, valor) # Caché
return valor
class Rectangulo:
def __init__(self, ancho, largo):
self.ancho = ancho
self.largo = largo
@PropiedadDiferida
def area(self):
print('Calculando área...')
return self.ancho * self.largo
r = Rectangulo(3, 4)
print(r.area) # Calcula y guarda en caché
print(r.area) # Usa el valor cacheado
Métodos estáticos y de clase
Los métodos estáticos y de clase también utilizan el protocolo de descriptores internamente.
Implementando @staticmethod
class MetodoEstatico:
def __init__(self, func):
self.func = func
def __get__(self, instancia, propietario):
return self.func
class Calculadora:
@MetodoEstatico
def sumar(x, y):
return x + y
print(Calculadora.sumar(2, 3)) # 5
print(Calculadora().sumar(4, 5)) # 9
Implementando @classmethod
class MetodoClase:
def __init__(self, func):
self.func = func
def __get__(self, instancia, propietario):
if propietario is None:
propietario = type(instancia)
def wrapper(*args, **kwargs):
return self.func(propietario, *args, **kwargs)
return wrapper
class Usuario:
nombre = 'invitado'
@MetodoClase
def saludar(cls):
print(f'Hola, {cls.nombre}')
Usuario.saludar() # Hola, invitado
Usuario().saludar() # Hola, invitadoclass Data(object):
def test(self):
print("test")
d = Data()
print d.test # Solo bound method pasa self implícitamente
print Data.test.__get__(d, Data) # Mismo resultado
print Data.test # Unbound method necesita self explícito
print Data.test.__get__(None, Data) # instance = None
Resumen de descriptores
Los descriptores son la base de muchas características de Python:
- @property - Define propiedades con getter, setter y deleter
- @staticmethod - Métodos que no requieren instancia
- @classmethod - Métodos que reciben la clase como primer argumento
- Funciones regulares - También son descriptores no de datos
- __slots__ - Utiliza descriptores internamente
El protocolo de descriptores permite implementar validaciones, cachés, propiedades calculadas y muchas otras características avanzadas en Python. Es una herramienta fundamental para el desarrollo de frameworks y bibliotecas que requieren control granular sobre el comportamiento de los atributos.