Python Properties

Nivel básico

Python Properties

Aprende a usar propiedades, getters y setters en Python de manera simple y práctica

Juan Vargas & Santiago Parra
20 minutos
Nivel básico

Mapa rápido

Propiedades básicas
4 min
Validación de datos
Decorador @property
5 min
Cálculos automáticos
Carrito avanzado
4 min
Sistema completo
Getters y Setters
4 min
Encapsulamiento
Descriptores
3 min
Nivel avanzado

Idea clave

¿Qué son las propiedades en Python?

Las propiedades nos permiten controlar cómo se accede y modifica la información en nuestros objetos, manteniendo una sintaxis simple como si fueran variables normales.

✅ Con propiedades

usuario.email = "juan@empresa.com"  # ✅ Válido
usuario.email = "correo-malo"        # ❌ Error automático

❌ Sin propiedades

usuario.email = "juan@gmail.com"       # ✅ Acepta cualquier cosa
# No hay validación automática

Ejemplo 1: Validación de correo

Problema: Validar correos institucionales
Queremos asegurar que solo se acepten correos con dominio @uexternado.edu.co
Código completo:
class Usuario:
    def __init__(self, nombre):
        self.nombre = nombre
        self._email = None  # Variable privada
    
    @property
    def email(self):
        """Getter: devuelve el email"""
        return self._email
    
    @email.setter
    def email(self, valor):
        """Setter: valida antes de guardar"""
        if not valor.endswith("@uexternado.edu.co"):
            raise ValueError("Debe ser un correo @uexternado.edu.co")
        self._email = valor

# Uso del código:
usuario = Usuario("Juan")
usuario.email = "juan.vargas@uexternado.edu.co"  # ✅ Funciona
print(f"Email guardado: {usuario.email}")

# Esto dará error:
# usuario.email = "juan@gmail.com"  # ❌ ValueError
Punto clave: Usamos _email (con guión bajo) para la variable interna, y email (sin guión) para la propiedad pública.

Ejemplo 2: Decorador @property

Cálculo automático de precios
El precio se redondea automáticamente cada vez que lo consultamos
Código completo:
class Producto:
    def __init__(self, nombre, precio_base):
        self.nombre = nombre
        self.precio_base = precio_base
        self.descuento = 0
    
    @property
    def precio_final(self):
        """Calcula el precio con descuento, redondeado"""
        precio_con_descuento = self.precio_base * (1 - self.descuento)
        return round(precio_con_descuento, 2)

# Uso del código:
producto = Producto("Laptop", 1299.99)
producto.descuento = 0.15  # 15% de descuento

print("Precio base: $" + str(producto.precio_base))
print("Descuento: " + str(producto.descuento * 100) + "%")
print("Precio final: $" + str(producto.precio_final))

# Cambiar descuento actualiza automáticamente el precio
producto.descuento = 0.25
print("Nuevo precio final: $" + str(producto.precio_final))
Ventaja: No necesitamos llamar una función como calcular_precio(). El precio se actualiza automáticamente cuando accedemos a producto.precio_final.

Ejemplo 3: Carrito de compras

Sistema completo con getter y setter
Un carrito que calcula subtotal, impuestos y total automáticamente
Código completo:
class Carrito:
    def __init__(self):
        self._productos = []
        self._tasa_impuesto = 0.19  # 19% IVA
    
    def agregar_producto(self, nombre, precio, cantidad=1):
        """Agrega un producto al carrito"""
        self._productos.append({
            'nombre': nombre,
            'precio': precio,
            'cantidad': cantidad
        })
    
    @property
    def subtotal(self):
        """Calcula el subtotal sin impuestos"""
        total = 0
        for producto in self._productos:
            total += producto['precio'] * producto['cantidad']
        return round(total, 2)
    
    @property
    def impuestos(self):
        """Calcula los impuestos sobre el subtotal"""
        return round(self.subtotal * self._tasa_impuesto, 2)
    
    @property
    def total(self):
        """Calcula el total con impuestos incluidos"""
        return round(self.subtotal + self.impuestos, 2)
    
    @property
    def tasa_impuesto(self):
        """Getter para la tasa de impuesto"""
        return self._tasa_impuesto
    
    @tasa_impuesto.setter
    def tasa_impuesto(self, nueva_tasa):
        """Setter para cambiar la tasa de impuesto"""
        if nueva_tasa < 0 or nueva_tasa > 1:
            raise ValueError("La tasa debe estar entre 0 y 1")
        self._tasa_impuesto = nueva_tasa

# Uso del código:
carrito = Carrito()
carrito.agregar_producto("Laptop", 1000, 1)
carrito.agregar_producto("Mouse", 25, 2)

print("Subtotal: $" + str(carrito.subtotal))
print("Impuestos (19%): $" + str(carrito.impuestos))
print("Total: $" + str(carrito.total))

# Cambiar tasa de impuesto
carrito.tasa_impuesto = 0.21  # 21%
print("\nCon 21% de impuestos:")
print("Impuestos: $" + str(carrito.impuestos))
print("Total: $" + str(carrito.total))
Salida esperada:
Subtotal: $1050.0
Impuestos (19%): $199.5
Total: $1249.5

Con 21% de impuestos:
Impuestos: $220.5
Total: $1270.5
Beneficio: Los cálculos se actualizan automáticamente. Si cambiamos la tasa de impuesto, el total se recalcula sin necesidad de llamar funciones adicionales.

Getters y Setters en Encapsulamiento

¿Qué es el encapsulamiento?
El encapsulamiento es ocultar los detalles internos de una clase y controlar cómo se accede a los datos

❌ Sin encapsulamiento

class CuentaBancaria:
    def __init__(self, titular):
        self.titular = titular
        self.saldo = 0  # Público, cualquiera puede modificar
        
# Problema: acceso directo sin control
cuenta = CuentaBancaria("Ana")
cuenta.saldo = -1000  # ❌ Saldo negativo permitido
cuenta.saldo = "texto"  # ❌ Tipo incorrecto permitido

✅ Con encapsulamiento

class CuentaBancaria:
    def __init__(self, titular):
        self.titular = titular
        self._saldo = 0  # Privado
        
    @property
    def saldo(self):
        return self._saldo
        
    @saldo.setter
    def saldo(self, valor):
        if not isinstance(valor, (int, float)):
            raise TypeError("El saldo debe ser un número")
        if valor < 0:
            raise ValueError("El saldo no puede ser negativo")
        self._saldo = valor

# Uso controlado
cuenta = CuentaBancaria("Ana")
cuenta.saldo = 1000  # ✅ Validado automáticamente
Ejemplo práctico: Sistema de calificaciones
Un sistema que valida calificaciones y calcula promedios automáticamente
Sistema completo de calificaciones:
class Estudiante:
    def __init__(self, nombre, email):
        self.nombre = nombre
        self.email = email
        self._calificaciones = []
    
    @property
    def email(self):
        """Getter para el email"""
        return self._email
    
    @email.setter
    def email(self, valor):
        """Setter que valida el formato del email"""
        if not isinstance(valor, str):
            raise TypeError("El email debe ser texto")
        if not valor.endswith("@uexternado.edu.co"):
            raise ValueError("Debe usar email institucional @uexternado.edu.co")
        if "@" not in valor or valor.count("@") != 1:
            raise ValueError("Formato de email inválido")
        self._email = valor
    
    @property
    def calificaciones(self):
        """Getter que devuelve copia de las calificaciones"""
        return self._calificaciones.copy()
    
    def agregar_calificacion(self, nota):
        """Método para agregar calificaciones con validación"""
        if not isinstance(nota, (int, float)):
            raise TypeError("La calificación debe ser un número")
        if not 0.0 <= nota <= 5.0:
            raise ValueError("La calificación debe estar entre 0.0 y 5.0")
        self._calificaciones.append(nota)
    
    @property
    def promedio(self):
        """Calcula el promedio automáticamente"""
        if not self._calificaciones:
            return 0.0
        return round(sum(self._calificaciones) / len(self._calificaciones), 2)
    
    @property
    def estado(self):
        """Determina si el estudiante está aprobado"""
        return "Aprobado" if self.promedio >= 3.0 else "Reprobado"

# Uso del sistema:
estudiante = Estudiante("María García", "maria.garcia@uexternado.edu.co")

# Agregar calificaciones
estudiante.agregar_calificacion(4.5)
estudiante.agregar_calificacion(3.8)
estudiante.agregar_calificacion(4.2)

print(f"Estudiante: {estudiante.nombre}")
print(f"Email: {estudiante.email}")
print(f"Calificaciones: {estudiante.calificaciones}")
print(f"Promedio actual: {estudiante.promedio}")
print(f"Estado: {estudiante.estado}")

# El promedio se actualiza automáticamente
estudiante.agregar_calificacion(4.0)
print(f"\nNueva calificación agregada: 4.0")
print(f"Promedio actualizado: {estudiante.promedio}")
Salida esperada:
Estudiante: María García
Email: maria.garcia@uexternado.edu.co
Calificaciones: [4.5, 3.8, 4.2]
Promedio actual: 4.17
Estado: Aprobado

Nueva calificación agregada: 4.0
Promedio actualizado: 4.12
Encapsulamiento: Los datos internos (_calificaciones, _email) están protegidos y solo se pueden modificar a través de métodos controlados.
Validación: Cada setter verifica que los datos sean válidos antes de guardarlos, evitando estados inconsistentes.
Cálculos automáticos: El promedio y estado se calculan dinámicamente cada vez que se consultan, siempre actualizados.
Ventajas del encapsulamiento con properties

🔒 Control de acceso

  • • Validación automática de datos
  • • Prevención de estados inválidos
  • • Protección de datos sensibles

⚡ Cálculos dinámicos

  • • Valores siempre actualizados
  • • No hay datos obsoletos
  • • Eficiencia en memoria

🔧 Mantenibilidad

  • • Cambios internos sin afectar el uso
  • • Código más organizado
  • • Fácil depuración

🎯 Sintaxis limpia

  • • Uso como variables normales
  • • No necesitas recordar métodos
  • • Código más legible

Descriptores

Nivel avanzado: Reutilizar validaciones
Los descriptores nos permiten crear validaciones que podemos usar en múltiples clases
Descriptor reutilizable:
class NumeroPositivo:
    """Descriptor que solo acepta números positivos"""
    
    def __init__(self, nombre):
        self.nombre = nombre
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, f'_{self.nombre}', 0)
    
    def __set__(self, obj, valor):
        if not isinstance(valor, (int, float)):
            raise TypeError(f"{self.nombre} debe ser un número")
        if valor < 0:
            raise ValueError(f"{self.nombre} debe ser positivo")
        setattr(obj, f'_{self.nombre}', valor)

class Producto:
    # Usamos el descriptor en múltiples atributos
    precio = NumeroPositivo('precio')
    stock = NumeroPositivo('stock')
    peso = NumeroPositivo('peso')
    
    def __init__(self, nombre):
        self.nombre = nombre

# Uso del código:
producto = Producto("Laptop")
producto.precio = 1000  # ✅ Funciona
producto.stock = 50     # ✅ Funciona
producto.peso = 2.5     # ✅ Funciona

print("Precio: $" + str(producto.precio))
print("Stock: " + str(producto.stock) + " unidades")
print("Peso: " + str(producto.peso) + " kg")

# Demostrar error:
try:
    producto.precio = -100  # ❌ ValueError
except ValueError as e:
    print("\nError al intentar precio negativo:")
    print(f"ValueError: {e}")
Salida esperada:
Precio: $1000
Stock: 50 unidades
Peso: 2.5 kg

Error al intentar precio negativo:
ValueError: precio debe ser positivo
Ventaja de los descriptores: Escribes la validación una vez y la reutilizas en múltiples atributos y clases. Es como tener un "molde" para crear propiedades con el mismo comportamiento.

Errores comunes

❌ Error 1: Recursión infinita

Código incorrecto:

class Usuario:
    @property
    def email(self):
        return self.email  # ❌ Se llama a sí mismo
    
    @email.setter
    def email(self, valor):
        self.email = valor  # ❌ Se llama a sí mismo

Código correcto:

class Usuario:
    @property
    def email(self):
        return self._email  # ✅ Variable privada
    
    @email.setter
    def email(self, valor):
        self._email = valor  # ✅ Variable privada
❌ Error 2: Olvidar el setter

Código incorrecto:

class Producto:
    @property
    def precio(self):
        return self._precio
    
    # ❌ Falta el setter
    
# Esto dará error:
# producto.precio = 100  # AttributeError

Código correcto:

class Producto:
    @property
    def precio(self):
        return self._precio
    
    @precio.setter  # ✅ Agregamos el setter
    def precio(self, valor):
        self._precio = valor

# Ahora funciona:
producto = Producto("Laptop")
producto.precio = 100  # ✅ Funciona
❌ Error 3: Nombres inconsistentes

Código incorrecto:

class Usuario:
    @property
    def email(self):
        return self._correo  # ❌ Nombres diferentes
    
    @email.setter
    def email(self, valor):
        self._email = valor  # ❌ Nombres diferentes

Código correcto:

class Usuario:
    @property
    def email(self):
        return self._email  # ✅ Consistente
    
    @email.setter
    def email(self, valor):
        self._email = valor  # ✅ Consistente

Bibliografía

Built with v0