CAP 09 · LEC 05·Programación orientada a objetos

@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.

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

@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 attribute
Salida78.53981633974483 31.41592653589793 10.0 314.1592653589793
@property no cambia la API pública

Este 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: -50
Salida999.99 1209.99 1200.0 Error: El precio no puede ser negativo: -50
Sin @propertyCon @property
obj.set_price(-10) # API rara obj.get_price() # verbosoobj.price = -10 # natural obj.price # limpio
La validación está en métodos separadosLa 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 cerrada
SalidaConectando a postgres://localhost/mydb... conexion_activa_postgres://localhost/mydb conexion_activa_postgres://localhost/mydb Cerrando conexión: conexion_activa_postgres://localhost/mydb None

Cuá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.

cached_property para valores costosos

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.0
Salida6.0 6.0 12.0

Practica