Python Properties: Getters y Setters | Capítulo 29

En Python, las properties son una forma elegante de encapsular atributos de clase, permitiendo que actúen como atributos normales mientras se controlan sus accesos mediante métodos. Imagina que tienes una caja fuerte: no quieres que cualquiera meta la mano directamente, pero sí permitir un acceso controlado. Las properties usan el decorador @property para definir getters y setters simples, promoviendo el principio de encapsulación sin complicar el código. En este capítulo, exploraremos cómo implementarlas paso a paso, enfocándonos en usos básicos sin validaciones complejas.

¿Qué Son las Properties y Por Qué Deberías Usarlas?

Las properties en Python te permiten definir métodos que se comportan como atributos. Esto significa que puedes acceder a ellos con la sintaxis simple de un atributo (como objeto.atributo), pero internamente ejecutan código personalizado. Es como tener una puerta automática: parece simple abrirla, pero hay mecanismos detrás que controlan el proceso.

Piensa en una analogía cotidiana: en una casa, no dejas las llaves expuestas; las guardas en un cajón con un candado simple. Las properties actúan como ese candado, protegiendo datos privados mientras mantienen la interfaz amigable. Sin ellas, podrías exponer atributos directamente, lo que viola la encapsulación. Con properties, mantienes el control sin sacrificar la legibilidad.

Antes de sumergirnos en el código, recuerda que las properties se basan en atributos privados (convencionalmente con un guion bajo, como _atributo). Esto no los hace verdaderamente privados en Python —ya que Python confía en la convención—, pero señala a otros desarrolladores que no deben acceder directamente.

Implementando un Getter con @property

Comencemos con lo básico: un getter. Este es un método que recupera el valor de un atributo privado, disfrazado como un atributo público. Usamos el decorador @property para lograrlo.

Supongamos que estamos modelando una clase Persona con un atributo de edad. Queremos que la edad sea accesible, pero no modificable directamente. Aquí va un ejemplo paso a paso.

Primero, crea un archivo llamado persona.py con el siguiente código:

class Persona:
    def __init__(self, edad):
        self._edad = edad  # Atributo privado: no acceder directamente desde fuera.

    @property
    def edad(self):
        """Getter para el atributo _edad."""
        return self._edad  # Simplemente devuelve el valor privado.

# Ejemplo de uso
p = Persona(30)
print(p.edad)  # Accede como un atributo, pero llama al getter internamente.
Python

Para ejecutar esto, abre tu terminal, navega al directorio del archivo y corre python persona.py. Verás que imprime 30. ¿Ves la magia? No llamas p.edad() como un método; solo usas p.edad. Esto hace que tu código sea más intuitivo y menos propenso a errores.

Explicación profunda: El decorador @property transforma el método edad en una property. Cuando accedes a p.edad, Python ejecuta el getter automáticamente. Si intentas asignar p.edad = 40 sin un setter, obtendrás un error —lo cual es intencional para control. Repito: esto promueve la encapsulación, asegurando que los cambios pasen por canales controlados.

Añadiendo un Setter para Controlar Asignaciones

Ahora, pasemos al setter, que te permite controlar cómo se asigna un valor. Usamos el decorador @edad.setter (donde edad es el nombre de la property). Esto es crucial para mantener la integridad de los datos sin validaciones complejas —solo un control básico.

Extiende el ejemplo anterior en persona.py:

class Persona:
    def __init__(self, edad):
        self._edad = edad  # Atributo privado.

    @property
    def edad(self):
        """Getter para el atributo _edad."""
        return self._edad

    @edad.setter
    def edad(self, nueva_edad):
        """Setter para actualizar _edad de forma controlada."""
        if nueva_edad < 0:  # Control simple: no permitimos edades negativas.
            raise ValueError("La edad no puede ser negativa.")
        self._edad = nueva_edad  # Actualiza el valor privado.

# Ejemplo de uso
p = Persona(30)
print(p.edad)  # Salida: 30

p.edad = 35  # Llama al setter internamente.
print(p.edad)  # Salida: 35

# Intenta algo inválido
try:
    p.edad = -5
except ValueError as e:
    print(e)  # Salida: La edad no puede ser negativa.
Python

Ejecuta de nuevo con python persona.py. Observa cómo p.edad = 35 parece una asignación simple, pero invoca el setter. Si intentas una edad negativa, lanza un error —un control básico sin complicaciones.

Paso a paso: El setter se activa con la sintaxis de asignación. Puedes agregar lógica simple aquí, como la verificación de no negativos. Recuerda, no estamos usando herramientas como Pydantic para validaciones avanzadas; nos mantenemos en lo esencial. Esto refuerza la idea: las properties hacen que tu clase sea más robusta sin alterar la interfaz.

Ejemplos Prácticos: Properties en Acción

Para dominar esto, veamos un ejemplo más realista. Imagina una clase Rectangulo donde el área se calcula dinámicamente, pero el ancho y alto son controlables.

Crea rectangulo.py:

class Rectangulo:
    def __init__(self, ancho, alto):
        self._ancho = ancho
        self._alto = alto

    @property
    def ancho(self):
        """Getter para ancho."""
        return self._ancho

    @ancho.setter
    def ancho(self, valor):
        """Setter para ancho con control básico."""
        if valor <= 0:
            raise ValueError("El ancho debe ser positivo.")
        self._ancho = valor

    @property
    def alto(self):
        """Getter para alto."""
        return self._alto

    @alto.setter
    def alto(self, valor):
        """Setter para alto con control básico."""
        if valor <= 0:
            raise ValueError("El alto debe ser positivo.")
        self._alto = valor

    @property
    def area(self):
        """Property de solo lectura para calcular área."""
        return self._ancho * self._alto  # No necesita setter, ya que es calculada.

# Ejemplo de uso
r = Rectangulo(5, 10)
print(r.area)  # Salida: 50

r.ancho = 20
print(r.area)  # Salida: 200

# Intenta inválido
try:
    r.alto = 0
except ValueError as e:
    print(e)  # Salida: El alto debe ser positivo.
Python

Ejecuta con python rectangulo.py. Aquí, area es una property de solo lectura —sin setter, no puedes asignarle directamente. Es como un medidor en tu auto: lo lees, pero no lo ajustas manualmente.

Analogía: Piensa en un termostato. Lees la temperatura (getter), ajustas el dial (setter con límites), y el sistema calcula el confort automáticamente. Las properties encapsulan esta lógica, haciendo tu código más mantenible.

Mejores Prácticas y Errores Comunes a Evitar

Como mentor, insisto: siempre usa atributos privados con _ para properties. No expongas datos crudos. Evita setters si no necesitas control —mantén properties de solo lectura para valores calculados.

Error común: Olvidar el decorador @property y llamar métodos como atributos, lo que falla. Otro: Intentar setters complejos; recuerda, nos limitamos a lo simple aquí.

Repito lo clave: Las properties no son magia; son una convención poderosa para encapsulación. Domínalas practicando: crea tu propia clase con properties y ejecútala. Sentirás el control que otorgan.

Resumen del Capítulo

  • Concepto Básico: Las properties permiten que métodos actúen como atributos, usando @property para getters y @nombre.setter para setters, promoviendo encapsulación simple.
  • Getter en Detalle: Define un método con @property para acceder a atributos privados de forma controlada, accesible como objeto.atributo.
  • Setter en Detalle: Agrega lógica básica para asignaciones con @nombre.setter, como verificaciones simples, invocado con objeto.atributo = valor.
  • Ejemplos Prácticos: Usadas en clases como Persona para edad controlada o Rectangulo para dimensiones y área calculada, ejecutables con python archivo.py.
  • Mejores Prácticas: Usa atributos privados, properties de solo lectura para cálculos, y evita exposiciones directas para mantener código robusto y legible.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio