W3docs

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 impossible

El 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._celsius

El 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 automatically

Como no hay setter, intentar asignarle un valor genera un error:

t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setter

Esta 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 = value

Ahora 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 zero

Regla 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 + 32

fahrenheit 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.0

Como 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 positive

Agregar 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 = -1

Má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 negative

Cualquier 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 years

La 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)     # None

Por 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 again

Siempre 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 attribute

Setter 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 negative

Las 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

SintaxisQué hace
@propertyDefine el getter; el atributo pasa a ser de solo lectura hasta que se agrega un setter
@<name>.setterDefine el setter; el atributo pasa a ser legible y escribible
@<name>.deleterDefine el deleter; del obj.attr activa este método
property(fget, fset, fdel, doc)Función integrada equivalente sin sintaxis de decorador
ClassName.prop.fgetLa función getter subyacente
ClassName.prop.fsetLa función setter subyacente (None si no hay setter)
ClassName.prop.fdelLa función deleter subyacente (None si no hay deleter)

Práctica

Práctica
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Práctica
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Práctica
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?