Los condicionales anidados obligan a seguir muchas ramas mentales al mismo tiempo. Cuando una función tiene varios niveles de indentación, suele ser difícil distinguir el flujo principal de los casos excepcionales.
En este tema practicaremos cláusulas de guarda y retornos tempranos. La idea es resolver primero los casos que impiden continuar y dejar el flujo principal con menos anidamiento.
Una cláusula de guarda es una condición al comienzo de una función o bloque que detecta un caso especial y termina la ejecución temprano.
def calcular_descuento(cliente):
if not cliente["activo"]:
return 0
return cliente["compras"] * 0.05
El caso que impide continuar queda claro al principio. Luego el flujo normal puede leerse sin un else innecesario.
Crea el archivo src/registro.py:
def registrar_usuario(usuario):
if usuario is not None:
if usuario.get("email"):
if "@" in usuario["email"]:
if usuario.get("edad", 0) >= 18:
if usuario.get("password"):
if len(usuario["password"]) >= 8:
return {
"estado": "registrado",
"mensaje": "Usuario registrado correctamente",
}
else:
return {
"estado": "error",
"mensaje": "La contraseña debe tener al menos 8 caracteres",
}
else:
return {"estado": "error", "mensaje": "La contraseña es obligatoria"}
else:
return {"estado": "error", "mensaje": "El usuario debe ser mayor de edad"}
else:
return {"estado": "error", "mensaje": "El email no tiene formato válido"}
else:
return {"estado": "error", "mensaje": "El email es obligatorio"}
else:
return {"estado": "error", "mensaje": "El usuario es obligatorio"}
La función valida datos, pero el camino exitoso está enterrado dentro de seis niveles de indentación.
Crea tests/test_registro.py:
from registro import registrar_usuario
def test_registra_usuario_valido():
usuario = {
"email": "ana@example.com",
"edad": 25,
"password": "secreto123",
}
assert registrar_usuario(usuario) == {
"estado": "registrado",
"mensaje": "Usuario registrado correctamente",
}
def test_rechaza_usuario_sin_email():
usuario = {
"email": "",
"edad": 25,
"password": "secreto123",
}
assert registrar_usuario(usuario) == {
"estado": "error",
"mensaje": "El email es obligatorio",
}
def test_rechaza_usuario_menor_de_edad():
usuario = {
"email": "ana@example.com",
"edad": 17,
"password": "secreto123",
}
assert registrar_usuario(usuario) == {
"estado": "error",
"mensaje": "El usuario debe ser mayor de edad",
}
Ejecuta:
python -m pytest tests/test_registro.py
La función tiene un caso exitoso y varios errores que impiden continuar:
Cada uno de esos casos puede transformarse en una cláusula de guarda.
Empezamos por los primeros errores y salimos temprano:
def registrar_usuario(usuario):
if usuario is None:
return {"estado": "error", "mensaje": "El usuario es obligatorio"}
if not usuario.get("email"):
return {"estado": "error", "mensaje": "El email es obligatorio"}
if "@" not in usuario["email"]:
return {"estado": "error", "mensaje": "El email no tiene formato válido"}
if usuario.get("edad", 0) >= 18:
if usuario.get("password"):
if len(usuario["password"]) >= 8:
return {
"estado": "registrado",
"mensaje": "Usuario registrado correctamente",
}
else:
return {
"estado": "error",
"mensaje": "La contraseña debe tener al menos 8 caracteres",
}
else:
return {"estado": "error", "mensaje": "La contraseña es obligatoria"}
else:
return {"estado": "error", "mensaje": "El usuario debe ser mayor de edad"}
Después de este paso ejecuta python -m pytest tests/test_registro.py.
Podemos seguir con las validaciones restantes:
def registrar_usuario(usuario):
if usuario is None:
return {"estado": "error", "mensaje": "El usuario es obligatorio"}
if not usuario.get("email"):
return {"estado": "error", "mensaje": "El email es obligatorio"}
if "@" not in usuario["email"]:
return {"estado": "error", "mensaje": "El email no tiene formato válido"}
if usuario.get("edad", 0) < 18:
return {"estado": "error", "mensaje": "El usuario debe ser mayor de edad"}
if not usuario.get("password"):
return {"estado": "error", "mensaje": "La contraseña es obligatoria"}
if len(usuario["password"]) < 8:
return {
"estado": "error",
"mensaje": "La contraseña debe tener al menos 8 caracteres",
}
return {
"estado": "registrado",
"mensaje": "Usuario registrado correctamente",
}
La función ahora tiene un flujo lineal: primero descarta errores, luego ejecuta el caso exitoso.
Cuando un if termina con return, muchas veces el else sobra.
if condicion:
return "error"
else:
return "ok"
Puede escribirse así:
if condicion:
return "error"
return "ok"
La segunda versión reduce indentación y deja más claro el flujo.
La función todavía repite diccionarios de error. Podemos extraer pequeñas funciones para mejorar consistencia:
def respuesta_error(mensaje):
return {"estado": "error", "mensaje": mensaje}
def respuesta_exitosa():
return {
"estado": "registrado",
"mensaje": "Usuario registrado correctamente",
}
Luego la función queda más breve:
def registrar_usuario(usuario):
if usuario is None:
return respuesta_error("El usuario es obligatorio")
if not usuario.get("email"):
return respuesta_error("El email es obligatorio")
if "@" not in usuario["email"]:
return respuesta_error("El email no tiene formato válido")
if usuario.get("edad", 0) < 18:
return respuesta_error("El usuario debe ser mayor de edad")
if not usuario.get("password"):
return respuesta_error("La contraseña es obligatoria")
if len(usuario["password"]) < 8:
return respuesta_error("La contraseña debe tener al menos 8 caracteres")
return respuesta_exitosa()
Si queremos dar todavía más intención, podemos extraer predicados:
def tiene_email_valido(usuario):
return bool(usuario.get("email")) and "@" in usuario["email"]
def es_mayor_de_edad(usuario):
return usuario.get("edad", 0) >= 18
def tiene_password_segura(usuario):
return bool(usuario.get("password")) and len(usuario["password"]) >= 8
No siempre hace falta extraer todos los predicados. Se justifica cuando el nombre mejora la comprensión o la regla se reutiliza.
Durante este refactoring el orden importa. Si un usuario tiene email inválido y es menor de edad, la función original devuelve primero el error de email. Cambiar el orden de las cláusulas de guarda podría cambiar el mensaje devuelto.
Por eso conviene tener pruebas para varios errores y respetar el orden del comportamiento actual, salvo que estemos haciendo explícitamente un cambio de reglas.
Podemos documentar un caso con múltiples errores:
def test_respeta_el_primer_error_segun_el_orden_actual():
usuario = {
"email": "email-sin-arroba",
"edad": 10,
"password": "",
}
assert registrar_usuario(usuario) == {
"estado": "error",
"mensaje": "El email no tiene formato válido",
}
Esta prueba evita que el refactoring cambie accidentalmente la prioridad de las validaciones.
Los retornos tempranos también ayudan en búsquedas. Observa este código:
def buscar_usuario_activo(usuarios, email):
encontrado = None
for usuario in usuarios:
if usuario["email"] == email:
if usuario["activo"]:
encontrado = usuario
return encontrado
Podemos simplificarlo:
def buscar_usuario_activo(usuarios, email):
for usuario in usuarios:
if usuario["email"] == email and usuario["activo"]:
return usuario
return None
El retorno temprano expresa que la búsqueda termina cuando encuentra el primer resultado válido.
Los retornos tempranos ayudan mucho en validaciones y casos excepcionales, pero no conviene abusar. Si una función tiene retornos dispersos en medio de muchas operaciones, puede ser difícil seguir el estado final.
Una regla práctica: usa cláusulas de guarda para cortar casos que impiden continuar. Para el flujo principal, intenta que la función siga siendo lineal y clara.
Crea el archivo src/reservas_validacion.py:
def validar_reserva(reserva):
if reserva is not None:
if reserva.get("cliente"):
if reserva.get("noches", 0) > 0:
if reserva.get("personas", 0) > 0:
if reserva["personas"] <= 6:
if reserva.get("pagada"):
return "reserva confirmada"
else:
return "la reserva debe estar pagada"
else:
return "la reserva supera el máximo de personas"
else:
return "la cantidad de personas debe ser mayor a cero"
else:
return "la cantidad de noches debe ser mayor a cero"
else:
return "el cliente es obligatorio"
else:
return "la reserva es obligatoria"
Realiza estas tareas:
else innecesarios después de retornos.python -m pytest después de cada paso.Una versión refactorizada puede ser:
def validar_reserva(reserva):
if reserva is None:
return "la reserva es obligatoria"
if not reserva.get("cliente"):
return "el cliente es obligatorio"
if reserva.get("noches", 0) <= 0:
return "la cantidad de noches debe ser mayor a cero"
if reserva.get("personas", 0) <= 0:
return "la cantidad de personas debe ser mayor a cero"
if reserva["personas"] > 6:
return "la reserva supera el máximo de personas"
if not reserva.get("pagada"):
return "la reserva debe estar pagada"
return "reserva confirmada"
La lógica es la misma, pero el flujo principal queda visible al final.
Antes de continuar, verifica que puedes explicar estos puntos:
else es innecesario después de un return.En este tema simplificamos condicionales anidados con cláusulas de guarda y retornos tempranos. El resultado fue código con menos indentación, errores tratados de forma explícita y un flujo principal más fácil de leer.
En el próximo tema veremos cómo eliminar duplicación sin caer en abstracciones prematuras.