@property: getters, setters y deleters sin boilerplate
@property convierte un método en un atributo calculado. Permite validar o transformar valores en la asignación sin cambiar la API pública de la clase.
@property básico (getter)
Decorar un método con @property lo convierte en un atributo de solo lectura. Se accede sin paréntesis, como un atributo normal, pero el valor se calcula cada vez que se accede.
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
@property
def area(self) -> float:
"""Área calculada — no se almacena, se recalcula en cada acceso."""
import math
return math.pi * self.radius ** 2
@property
def circumference(self) -> float:
import math
return 2 * math.pi * self.radius
@property
def diameter(self) -> float:
return self.radius * 2
c = Circle(5.0)
# Se accede como atributo, sin paréntesis
print(c.area) # 78.53981633974483
print(c.circumference) # 31.41592653589793
print(c.diameter) # 10.0
# Si cambia el radio, los valores calculados se actualizan automáticamente
c.radius = 10.0
print(c.area) # 314.1592653589793
# Intentar asignar lanza AttributeError (solo getter)
# c.area = 100 # AttributeError: can't set attribute78.53981633974483
31.41592653589793
10.0
314.1592653589793Este es el gran beneficio: puedes empezar con un atributo directo (self.area = ...) y luego convertirlo a @property sin que ningún código que use la clase tenga que cambiar. La transición es transparente.
@setter con validación
El setter permite controlar qué valores se asignan. Se define con @nombre_property.setter. Si alguien intenta asignar un valor inválido, puedes lanzar una excepción.
class Product:
def __init__(self, name: str, price: float) -> None:
self.name = name
self.price = price # llama al setter, incluso en __init__
@property
def price(self) -> float:
return self._price # atributo interno con prefijo _
@price.setter
def price(self, value: float) -> None:
if value < 0:
raise ValueError(f"El precio no puede ser negativo: {value}")
self._price = value
@property
def price_with_tax(self) -> float:
return round(self._price * 1.21, 2) # IVA 21%
p = Product("Laptop", 999.99)
print(p.price) # 999.99
print(p.price_with_tax) # 1209.99
p.price = 1200.0 # funciona
print(p.price) # 1200.0
try:
p.price = -50 # lanza ValueError
except ValueError as e:
print(f"Error: {e}") # Error: El precio no puede ser negativo: -50999.99
1209.99
1200.0
Error: El precio no puede ser negativo: -50| Sin @property | Con @property |
|---|---|
| obj.set_price(-10) # API rara obj.get_price() # verboso | obj.price = -10 # natural obj.price # limpio |
| La validación está en métodos separados | La validación está en el setter |
@deleter
El deleter se llama cuando se usa del obj.atributo. Se define con @nombre_property.deleter. Útil para limpiar recursos o resetear estado al eliminar una propiedad.
class DatabaseConnection:
def __init__(self, url: str) -> None:
self._url = url
self._connection = None
@property
def connection(self):
if self._connection is None:
print(f"Conectando a {self._url}...")
self._connection = f"conexion_activa_{self._url}"
return self._connection
@connection.setter
def connection(self, value) -> None:
self._connection = value
@connection.deleter
def connection(self) -> None:
"""Cierra y limpia la conexión."""
if self._connection is not None:
print(f"Cerrando conexión: {self._connection}")
self._connection = None
db = DatabaseConnection("postgres://localhost/mydb")
print(db.connection) # Conectando a... (lazy connection)
print(db.connection) # ya conectado, no reconecta
del db.connection # llama al deleter
print(db._connection) # None — conexión cerradaConectando a postgres://localhost/mydb...
conexion_activa_postgres://localhost/mydb
conexion_activa_postgres://localhost/mydb
Cerrando conexión: conexion_activa_postgres://localhost/mydb
NoneCuándo usar property vs atributo directo
No todo necesita una property. El criterio es si necesitas lógica adicional al leer o escribir el valor.
functools.cached_property es como @property pero guarda el resultado en caché al primer acceso. Perfecto para cálculos costosos que no cambian durante la vida del objeto.
from functools import cached_property
import math
class Triangle:
def __init__(self, a: float, b: float, c: float) -> None:
if a + b <= c or a + c <= b or b + c <= a:
raise ValueError("Lados inválidos para un triángulo")
self.a = a
self.b = b
self.c = c
@cached_property
def area(self) -> float:
"""Fórmula de Herón — se calcula una sola vez y se cachea."""
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
@cached_property
def perimeter(self) -> float:
return self.a + self.b + self.c
t = Triangle(3, 4, 5)
print(t.area) # 6.0 (calculado)
print(t.area) # 6.0 (desde caché, sin recalcular)
print(t.perimeter) # 12.06.0
6.0
12.0