Antes de explicar el decorador @property vamos a disponer un ejemplo y mostrar el tipo de problema que nos resolverá este decorador.
Plantear una clase dado.
import random class Dado: def __init__(self): self.valor = 1 self.tirar() def tirar(self): self.valor = random.randint(1, 6) def imprimir(self): print("El valor del dado es:", self.valor) dado1 = Dado() dado1.tirar() dado1.imprimir() dado1.valor = 50 dado1.imprimir()
En muchas situaciones queremos que un atributo no sea modificado desde fuera de la clase, es lógico que un dado solo puede almacenar valores entre 1 y 6.
Desde fuera de la clase estamos modificando por un valor que no puede tener un dado:
dado1.valor = 50
En el lenguaje Python los atributos de una clase siempre son públicos y accesibles desde fuera, veremos como mediante el decorador @property podemos tener una solución elegante a este problema.
Plantear una clase dado. Ocultar el atributo que almacena el valor del dado y exponerlo a través de una propiedad.
import random class Dado: def __init__(self): self._valor = 1 def tirar(self): self.valor = random.randint(1, 6) @property def valor(self): return self._valor @valor.setter def valor(self, nuevo_valor): if nuevo_valor >=1 and nuevo_valor <= 6: self._valor = nuevo_valor else: raise ValueError("Error: El valor del dado debe estar entre 1 y 6.") dado1 = Dado() dado1.tirar() print("Valor del dado",dado1.valor) dado1.valor = 3 print("Valor del dado",dado1.valor) dado1.valor = 50 # Esto genera un error
Hemos creado el atributo _valor donde almacenamos el valor del dado.
¿Por qué usamos _valor en lugar de valor?
En Python no existe la encapsulación estricta (como private o protected en Java o C++).
En su lugar, se usa una convención de nombres:
atributo (sin _) Se considera público. Se puede acceder libremente desde fuera de la clase.
_atributo (con un _ al inicio) Se considera protegido/privado por convención.
Es una forma de decirle a otros programadores:
"Este atributo es interno, no deberías accederlo directamente; usá propiedades."
class Dado: def __init__(self): self._valor = 1 # atributo interno
Usamos _valor para indicar que es un atributo interno que no debería tocarse desde afuera.
En lugar de eso, exponemos una property llamada valor:
@property def valor(self): return self._valor
Así, el usuario de la clase escribe dado1.valor como si accediera a un atributo, pero en realidad lo que está pasando es que se ejecuta el método valor().
print("Valor del dado",dado1.valor)
Esto permite ocultar la implementación interna y tener control.
¿Qué es @valor.setter?
@valor.setter def valor(self, nuevo_valor): if nuevo_valor >=1 and nuevo_valor <= 6: self._valor = nuevo_valor else: raise ValueError("Error: El valor del dado debe estar entre 1 y 6.")
En Python, cuando definís una propiedad con @property, por defecto esa propiedad es solo de lectura (o sea, podés acceder al valor pero no modificarlo), nos daría error:
dado1.valor = 3
Si querés permitir la escritura controlada de ese atributo, usás el decorador @<nombre>.setter, donde <nombre> debe coincidir con el de la propiedad:
@property def valor(self): return self._valor @valor.setter def valor(self, nuevo_valor): if nuevo_valor >=1 and nuevo_valor <= 6: self._valor = nuevo_valor else: raise ValueError("Error: El valor del dado debe estar entre 1 y 6.")
Con @valor.setter podemos validar que el dato a almacenar en el atributo sea válido, logramos tener una clase más robusta.
Debe quedar claro que la propiedad se llama valor y el atributo _valor.
En Python, una propiedad derivada es un atributo de una clase cuyo valor se calcula a partir de otros atributos, en lugar de almacenarse directamente.
Se crean usando @property.
No existe un atributo interno asociado necesariamente; su valor se devuelve calculado cada vez que se accede.
Son útiles para exponer información calculada o resumida de manera limpia, sin que el usuario tenga que llamar a un método.
Crear la clase Cuadrado con dos propiedades derivadas: superficie y perimetro
class Cuadrado: def __init__(self, lado): self._lado = lado # atributo interno @property def lado(self): return self._lado @lado.setter def lado(self, nuevo_lado): if nuevo_lado > 0: self._lado = nuevo_lado else: raise ValueError("El lado debe ser mayor que 0") @property def superficie(self): """Propiedad derivada: calcula el área del cuadrado""" return self._lado ** 2 @property def perimetro(self): """Propiedad derivada: calcula el perímetro del cuadrado""" return 4 * self._lado c = Cuadrado(5) print("Lado:", c.lado) print("Superficie:", c.superficie) # 25 print("Perímetro:", c.perimetro) # 20 c.lado = 10 print("Nuevo lado:", c.lado) print("Superficie:", c.superficie) # 100 print("Perímetro:", c.perimetro) # 40
_lado es un atributo interno que almacena la medida del cuadrado.
lado es la propiedad con getter y setter que permite controlar la asignación.
superficie es la propiedad derivada, calcula lado ** 2:
@property def superficie(self): """Propiedad derivada: calcula el área del cuadrado""" return self._lado ** 2
perimetro es la propiedad derivada, calcula 4 * lado.
@property def perimetro(self): """Propiedad derivada: calcula el perímetro del cuadrado""" return 4 * self._lado
Ventaja: aunque el lado cambie, las propiedades derivadas siempre reflejan el estado actual sin necesidad de almacenarlas.
Luego las accedemos a partir del objeto por su nombre:
c = Cuadrado(5) print("Lado:", c.lado) print("Superficie:", c.superficie) # 25 print("Perímetro:", c.perimetro) # 20
Queremos modelar un dispositivo con batería que tiene las siguientes reglas:
La batería tiene un nivel de carga entre 0 y 100.
No se puede asignar un valor fuera de ese rango (se lanza un error).
Cada vez que se carga la batería, se actualiza el nivel.
Cuando se descarga, el nivel no puede ser negativo.
Se puede consultar el estado de la batería como porcentaje y también si está casi vacía (<20%).
Usaremos @property para:
Encapsular el atributo interno _nivel de la batería.
Controlar la asignación de valores con validación.
Crear una propiedad derivada casi_vacia que dependa del nivel.
class Bateria: def __init__(self): self._nivel = 100 # nivel inicial 100% @property def nivel(self): return self._nivel @nivel.setter def nivel(self, nuevo_valor): if nuevo_valor >= 0 and nuevo_valor <= 100: self._nivel = nuevo_valor else: raise ValueError("Error: el nivel de batería debe estar entre 0 y 100.") @property def casi_vacia(self): return self._nivel < 20 def cargar(self, cantidad): print(f"Cargando {cantidad}%...") self.nivel = min(self._nivel + cantidad, 100) def descargar(self, cantidad): print(f"Descargando {cantidad}%...") self.nivel = max(self._nivel - cantidad, 0) # Prueba b = Bateria() print("Nivel inicial:", b.nivel) b.descargar(85) print("Nivel después de descarga:", b.nivel) print("¿Batería casi vacía?", b.casi_vacia) b.cargar(50) print("Nivel después de carga:", b.nivel)
Creamos la propiedad derivada llamada casi_vacia que retorna True si el nivel de carga de la batería es menor de 20:
@property def casi_vacia(self): return self._nivel < 20
Luego llamamos al método desde el bloque principal:
print("¿Batería casi vacía?", b.casi_vacia)
Como vemos en ningún atribulo almacenamos si la batería esta casi vacía, se la calcula según el nivel.