El decorador @property permite que un método se comporte como un atributo: se accede sin paréntesis, pero por debajo ejecuta código. Esto te permite añadir validación, conversiones o cálculos sin cambiar la interfaz pública de la clase ni obligar a los usuarios a usar métodos get_algo() y set_algo() al estilo Java.
Getter básico con @property
class Circulo:
def __init__(self, radio):
self._radio = radio # convención: _ indica uso interno
@property
def radio(self):
return self._radio
@property
def area(self): # propiedad computada (solo lectura)
import math
return math.pi * self._radio ** 2
@property
def diametro(self):
return self._radio * 2
c = Circulo(5)
print(c.radio) # 5 acceso sin paréntesis
print(c.area) # 78.53...
print(c.diametro) # 10
# c.radio = 3 # AttributeError no hay setter todavía
Setter con validación
class Temperatura:
def __init__(self, celsius):
self.celsius = celsius # usa el setter desde __init__
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, valor):
if valor < -273.15:
raise ValueError(f"Temperatura imposible: {valor}°C")
self._celsius = valor
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, valor):
self.celsius = (valor - 32) * 5 / 9 # usa el setter de celsius
t = Temperatura(25)
print(t.fahrenheit) # 77.0
t.fahrenheit = 98.6
print(t.celsius) # 37.0
# t.celsius = -300 # ValueError: Temperatura imposible: -300°C
Deleter
class Cache:
def __init__(self):
self._datos = {}
@property
def datos(self):
return self._datos.copy()
@datos.setter
def datos(self, nuevo):
if not isinstance(nuevo, dict):
raise TypeError("Se esperaba un diccionario")
self._datos = nuevo
@datos.deleter
def datos(self):
self._datos = {}
print("Caché limpiado")
c = Cache()
c.datos = {"clave": "valor"}
print(c.datos) # {'clave': 'valor'}
del c.datos # Caché limpiado
print(c.datos) # {}
Propiedades computadas en dataclass
from dataclasses import dataclass
@dataclass
class Rectangulo:
ancho: float
alto: float
@property
def area(self):
return self.ancho * self.alto
@property
def perimetro(self):
return 2 * (self.ancho + self.alto)
@property
def es_cuadrado(self):
return self.ancho == self.alto
r = Rectangulo(4, 6)
print(r.area) # 24
print(r.perimetro) # 20
print(r.es_cuadrado) # False
El antipatrón a evitar
# MAL: getters y setters sin lógica, estilo Java
class Producto:
def get_nombre(self):
return self._nombre
def set_nombre(self, valor):
self._nombre = valor
# BIEN: acceso directo si no hay lógica; @property si hay validación
class Producto:
def __init__(self, nombre):
self.nombre = nombre # acceso directo simple y claro
Empieza con atributos públicos simples. Añade @property cuando necesites lógica de validación o computación sin romper la interfaz. Ese es exactamente el escenario para el que fue diseñado.
