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 minValidación de datos
Decorador @property
5 minCálculos automáticos
Carrito avanzado
4 minSistema completo
Getters y Setters
4 minEncapsulamiento
Descriptores
3 minNivel 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áticaEjemplo 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" # ❌ ValueErrorPunto 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.5Beneficio: 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áticamenteEjemplo 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.12Encapsulamiento: 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 positivoVentaja 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í mismoCó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 # AttributeErrorCó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 diferentesCódigo correcto:
class Usuario:
@property
def email(self):
return self._email # ✅ Consistente
@email.setter
def email(self, valor):
self._email = valor # ✅ Consistente