Python @property: Getters y Setters
Aprende el decorador @property de Python: crea getters, setters, deleters y atributos calculados con sintaxis limpia y control total de validación.
El decorador @property es el mecanismo integrado de Python para convertir un método en un atributo gestionado. En lugar de escribir métodos get_x() y set_x() como en otros lenguajes, escribes un acceso a atributo de aspecto normal (obj.x) mientras mantienes control total sobre lo que ocurre cuando ese atributo se lee, se escribe o se elimina.
Este capítulo cubre:
- Por qué existen las propiedades y cuándo usarlas
- Crear una propiedad de solo lectura con
@property - Agregar un setter con
@<name>.setter - Agregar un deleter con
@<name>.deleter - Propiedades calculadas (derivadas)
- Actualizar un atributo simple a una propiedad sin romper el código existente
- La función integrada
property()— el mecanismo subyacente del decorador - Cómo funcionan las propiedades como descriptores (un breve vistazo bajo el capó)
- Errores comunes
Antes de leer, asegúrate de estar familiarizado con las clases y objetos de Python. Las propiedades son una herramienta clave para la encapsulación en Python. Para métodos de clase y estáticos, consulta @staticmethod y @classmethod.
Por qué Existen las Propiedades
Considera una clase que almacena una temperatura en Celsius. Una implementación ingenua expone el valor interno directamente:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t = Temperature(25)
t.celsius = -5000 # nothing stops this — physically impossibleEl problema: nada impide que los llamadores establezcan una temperatura por debajo del cero absoluto (−273.15 °C). Podrías agregar un método set_celsius() con validación, pero entonces los llamadores tendrían que cambiar su código de t.celsius = 100 a t.set_celsius(100) — un cambio de API que rompe la compatibilidad.
@property resuelve esto de forma elegante. Mantienes la sintaxis t.celsius = 100 mientras añades una capa de control detrás de escena.
Getter Básico: Acceso de Solo Lectura
El uso más simple de @property es un atributo de solo lectura respaldado por una variable privada:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # store in a private attribute
@property
def celsius(self):
return self._celsiusEl decorador @property hace que celsius parezca un atributo simple para el llamador:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses; Python calls the getter automaticallyComo no hay setter, intentar asignarle un valor genera un error:
t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setterEsta es la forma correcta de modelar un valor que solo debe establecerse en el momento de la construcción o a través de métodos específicos.
Agregar un Setter con Validación
Decora un segundo método con @<property_name>.setter para gestionar las escrituras:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = valueAhora tanto la lectura como la escritura funcionan con sintaxis de atributo simple:
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 100
print(t.celsius) # 100
t.celsius = -300 # ValueError: Temperature below absolute zeroRegla clave: el setter y el getter deben compartir el mismo nombre (celsius en ambos casos). El decorador @celsius.setter vincula el nuevo método al objeto de propiedad celsius existente.
Propiedades Calculadas
Una propiedad no tiene que corresponder a un atributo almacenado. Puede calcular un valor en tiempo real a partir de otros datos:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32fahrenheit no tiene variable de respaldo — deriva su valor de _celsius cada vez que se lee:
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.celsius = 100
print(t.fahrenheit) # 212.0Como no hay @fahrenheit.setter, intentar escribir t.fahrenheit = 100 genera un AttributeError. Las propiedades calculadas son de solo lectura por naturaleza a menos que añadas explícitamente un setter.
Un ejemplo real de propiedad calculada
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('Width must be positive')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('Height must be positive')
self._height = value
@property
def area(self):
return self._width * self._height # computed; no setter
@property
def perimeter(self):
return 2 * (self._width + self._height) # computed; no setter
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
r.width = 10
print(r.area) # 50
r.width = -1 # ValueError: Width must be positiveAgregar un Deleter
El decorador @<property_name>.deleter te permite ejecutar código cuando el llamador usa del obj.attr:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@celsius.deleter
def celsius(self):
print('Deleting celsius')
del self._celsius
t = Temperature(25)
del t.celsius # Deleting celsius
print(t.celsius) # AttributeError: 'Temperature' object has no attribute '_celsius'Los deleters se usan con menos frecuencia que los getters y setters. Son útiles cuando:
- Se elimina un valor en caché para forzar el recálculo en el próximo acceso.
- Se liberan explícitamente recursos vinculados a un atributo.
- Se garantiza que, una vez eliminado, un valor no pueda volver a leerse sin reasignarlo.
Actualizar un Atributo Simple a una Propiedad
Uno de los mayores beneficios prácticos de @property es que puedes comenzar con un atributo público simple y agregar validación después sin cambiar el código que lo llama. Esto se conoce a veces como el principio de acceso uniforme.
# Version 1 — plain attribute, no validation
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # works, but nothing stops c.radius = -1Más adelante necesitas validación. Con @property puedes agregarla sin modificar los llamadores:
# Version 2 — property with validation; public interface unchanged
import math
class Circle:
def __init__(self, radius):
self.radius = radius # this now calls the setter
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
@property
def area(self):
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(f'{c.area:.4f}') # 78.5398
c.radius = 10
print(c.radius) # 10
c.radius = -1 # ValueError: Radius cannot be negativeCualquier código existente que lea o escriba c.radius seguirá funcionando sin modificaciones.
La Función Integrada property()
@property es azúcar sintáctico para la función integrada property(). Estas dos definiciones son equivalentes:
# --- decorator style (recommended) ---
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value# --- property() style (explicit) ---
class Person:
def __init__(self, age):
self._age = age
def _get_age(self):
return self._age
def _set_age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value
def _del_age(self):
del self._age
age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')property(fget, fset, fdel, doc) acepta hasta cuatro argumentos: una función getter, una función setter, una función deleter y una cadena de documentación. Cualquiera de ellos puede ser None.
p = Person(30)
print(p.age) # 30
p.age = 31
print(p.age) # 31
print(Person.age.__doc__) # The person's age in yearsLa forma con decorador es más limpia y es la recomendación estándar. La llamada explícita a property() es útil cuando quieres pasar la cadena de documentación sin un bloque de decorador de varias líneas, o cuando las funciones de acceso ya existen con otro nombre.
Cómo Funcionan las Propiedades: Un Breve Vistazo a los Descriptores
Internamente, property es un descriptor — un objeto que define __get__, __set__ y __delete__ en la clase. Cuando Python busca obj.attr, comprueba si el atributo en la clase es un descriptor y, si es así, llama a su __get__ en lugar de devolver el valor directamente.
Puedes ver esto inspeccionando el objeto de propiedad en la clase:
class Square:
def __init__(self, side):
self._side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if value < 0:
raise ValueError('Side must be non-negative')
self._side = value
print(type(Square.side)) # <class 'property'>
print(Square.side.fget) # <function Square.side at 0x...>
print(Square.side.fset) # <function Square.side at 0x...>
print(Square.side.fdel) # NonePor eso leer Square.side devuelve el objeto de propiedad en sí (descriptor accedido en la clase), mientras que leer s.side en una instancia activa __get__ y devuelve el entero. El protocolo de descriptores es el mismo mecanismo que utilizan classmethod, staticmethod y las funciones en sí. Para una exploración más profunda, consulta los métodos mágicos de Python.
Errores Comunes
Recursión infinita: olvidar el guión bajo
Un error muy común es usar el mismo nombre para la propiedad y el atributo de respaldo:
class Bad:
@property
def value(self):
return self.value # RecursionError! This calls the getter again
@value.setter
def value(self, v):
self.value = v # RecursionError! This calls the setter againSiempre almacena el valor de respaldo con un nombre diferente, por convención con un guión bajo como prefijo:
class Good:
@property
def value(self):
return self._value # reads the private attribute
@value.setter
def value(self, v):
self._value = v # writes the private attributeSetter definido antes que el getter
El decorador del setter @celsius.setter hace referencia al objeto de propiedad celsius, que debe existir primero. Define siempre el getter (@property) antes que el setter y el deleter en el cuerpo de la clase.
__init__ llama al setter automáticamente
Cuando escribes self.radius = radius dentro de __init__, Python llama al setter (si existe). Esto generalmente es lo que deseas — la validación también se ejecuta en el momento de la construcción. Pero significa que tu setter debe gestionar la asignación inicial correctamente:
class Circle:
def __init__(self, radius):
self.radius = radius # triggers the setter — validation applies here too
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
Circle(-1) # ValueError: Radius cannot be negativeLas propiedades son a nivel de clase, no de instancia
No puedes agregar una propiedad a una sola instancia como lo harías con atributos regulares. Las propiedades se definen en la clase y se aplican a todas las instancias. Si necesitas personalización de atributos por instancia, consulta los dataclasses de Python o usa un enfoque basado en __slots__.
Referencia Rápida
| Sintaxis | Qué hace |
|---|---|
@property | Define el getter; el atributo pasa a ser de solo lectura hasta que se agrega un setter |
@<name>.setter | Define el setter; el atributo pasa a ser legible y escribible |
@<name>.deleter | Define el deleter; del obj.attr activa este método |
property(fget, fset, fdel, doc) | Función integrada equivalente sin sintaxis de decorador |
ClassName.prop.fget | La función getter subyacente |
ClassName.prop.fset | La función setter subyacente (None si no hay setter) |
ClassName.prop.fdel | La función deleter subyacente (None si no hay deleter) |