Depuración en Python

PythonBeginner
Practicar Ahora

Introducción

En la programación informática, la depuración (debugging) es el proceso esencial de encontrar y resolver defectos o "bugs" que impiden que un programa funcione correctamente. Si bien los bugs son una parte inevitable del desarrollo de software, Python proporciona un potente conjunto de herramientas para ayudarle a diagnosticarlos y corregirlos de manera eficiente.

En este laboratorio, aprenderá varias técnicas fundamentales de depuración en Python. Cubriremos cómo:

  • Lanzar y manejar excepciones manualmente para controlar el flujo del programa durante los errores.
  • Capturar tracebacks de error detallados como cadenas (strings) para fines de registro (logging).
  • Usar aserciones (assertions) como comprobaciones de cordura (sanity checks) para detectar errores lógicos tempranamente.
  • Implementar un sistema de registro (logging) flexible para rastrear la ejecución de su programa y diagnosticar problemas sin interrumpir su flujo.

Lanzamiento de Excepciones

En este paso, aprenderás a activar excepciones manualmente usando la sentencia raise. Las excepciones son una forma de señalar que ha ocurrido un error. Una sentencia raise consiste en la palabra clave raise, una llamada a la función Exception(), y un mensaje de error útil pasado a Exception().

Es una práctica común lanzar excepciones dentro de una función cuando sus precondiciones no se cumplen, y luego manejar estas excepciones en el código que llama a la función usando bloques try y except.

Nota: Si no estás familiarizado con el uso de WebIDE (similar a VS Code), por favor, apréndelo primero en este laboratorio.

Probemos esto con un ejemplo.

  1. Abre el archivo raise_exception.py desde el explorador de archivos de la izquierda.
  2. Añade el siguiente código al archivo. Este código define una función box_print que lanza excepciones para argumentos inválidos y luego llama a esta función con entradas válidas e inválidas, capturando las excepciones.
def box_print(symbol, width, height):
    if len(symbol) != 1:
      raise Exception('Symbol must be a single character string.')
    if width <= 2:
      raise Exception('Width must be greater than 2.')
    if height <= 2:
      raise Exception('Height must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

## Handle exceptions when calling the function
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        box_print(sym, w, h)
    except Exception as err:  ## Catch exception and print error message
        print('An exception happened: ' + str(err))
  1. Ahora, ejecuta el script desde la terminal para ver cómo se lanzan y manejan las excepciones.
python3 raise_exception.py

Verás la siguiente salida. Las dos primeras llamadas a box_print tienen éxito, mientras que las dos últimas fallan, activando el bloque except para imprimir los mensajes de error personalizados.

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

Obtener el Traceback como una Cadena de Texto

Cuando ocurre una excepción no manejada, Python imprime un traceback (rastreo de pila), que es un informe detallado de las llamadas a funciones que condujeron al error. A veces, desea manejar la excepción con elegancia pero aun así registrar el traceback completo para una depuración posterior. El módulo traceback le permite hacer esto.

En este paso, aprenderá a usar traceback.format_exc() para capturar el traceback como una cadena (string) y guardarlo en un archivo.

  1. Abra el archivo get_traceback.py desde el explorador de archivos.
  2. Agregue el siguiente código. Este script importa el módulo traceback, luego usa un bloque try...except para capturar una excepción. Dentro del bloque except, escribe el traceback completo en un archivo llamado errorInfo.txt.
import traceback

try:
    raise Exception('This is the error message.')
except:
    with open('errorInfo.txt', 'w') as error_file:
        error_file.write(traceback.format_exc())  ## Write traceback to file
    print('The traceback info was written to errorInfo.txt.')
  1. Ejecute el script en la terminal.
python3 get_traceback.py

El script imprimirá un mensaje de confirmación:

The traceback info was written to errorInfo.txt.
  1. Ahora, verifique el contenido del archivo recién creado errorInfo.txt. Puede abrirlo desde el explorador de archivos o usar el comando cat en la terminal.
cat errorInfo.txt

El archivo contendrá el traceback completo, similar a esto:

Traceback (most recent call last):
  File "/home/labex/project/get_traceback.py", line 4, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

Esta técnica es extremadamente útil para registrar errores en aplicaciones de producción sin que el programa se bloquee (crash).

Uso de Asertos (Assertions)

Los asertos son verificaciones de cordura (sanity checks) para asegurar que su código no está haciendo algo obviamente incorrecto. Una sentencia assert comprueba si una condición es verdadera. Si la condición es falsa, lanza una AssertionError, lo que idealmente debería hacer que su programa falle. Este enfoque de "fallar rápido" (fail-fast) le ayuda a identificar la causa raíz de un error más rápidamente.

Una sentencia assert consiste en la palabra clave assert, una condición a verificar, y un mensaje de error opcional.

En este paso, usará un aserto para validar el estado de un programa.

  1. Abra el archivo use_assertions.py desde el explorador de archivos.
  2. Agregue el siguiente código. Comprueba el estado de pod_bay_door_status. El primer aserto pasa silenciosamente, pero el segundo fallará y lanzará una AssertionError.
## assert statement: check condition, raise AssertionError if False
pod_bay_door_status = 'open'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Passes

pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Raises AssertionError
  1. Ejecute el script desde la terminal.
python3 use_assertions.py

Debido a que el segundo aserto falla, el programa se detendrá y verá una AssertionError con su mensaje personalizado.

Traceback (most recent call last):
  File "/home/labex/project/use_assertions.py", line 6, in <module>
    assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
AssertionError: The pod bay doors need to be "open".

A diferencia de las excepciones, no debe manejar los asertos con try...except. Una AssertionError señala un error (bug) en el programa que necesita ser corregido. También vale la pena señalar que los asertos pueden ser deshabilitados globalmente ejecutando Python con la bandera -O (optimizar), por lo que no deben usarse para la validación de entradas que siempre deben aplicarse.

Implementación de Registro (Logging)

Si bien las sentencias print() se pueden usar para la depuración, un enfoque más potente y flexible es utilizar el módulo logging incorporado de Python. El registro (logging) le permite rastrear eventos a medida que su programa se ejecuta y clasificar los mensajes por severidad.

En este paso, agregará registro a un programa para ayudar a diagnosticar un error (bug).

  1. Primero, abra el archivo factorial_log.py desde el explorador de archivos.
  2. Agregue el siguiente código. Este programa intenta calcular el factorial de un número pero contiene un error. Hemos añadido llamadas a logging.debug() para rastrear los valores de las variables en cada paso. La función logging.basicConfig() configura el sistema de registro para mostrar mensajes de nivel DEBUG y superior.
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s)' % (n))
    total = 1
    for i in range(1, n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s)' % (n))
    return total

print(factorial(5))
logging.debug('End of program')
  1. Ejecute el script desde la terminal.
python3 factorial_log.py

Verá una salida de registro detallada intercalada con la salida regular del programa.

 %(asctime)s - DEBUG - Start of program
 %(asctime)s - DEBUG - Start of factorial(5)
 %(asctime)s - DEBUG - i is 1, total is 1
 %(asctime)s - DEBUG - i is 2, total is 2
 %(asctime)s - DEBUG - i is 3, total is 6
 %(asctime)s - DEBUG - i is 4, total is 24
 %(asctime)s - DEBUG - i is 5, total is 120
 %(asctime)s - DEBUG - End of factorial(5)
120
 %(asctime)s - DEBUG - End of program

Al examinar los mensajes de registro, puede ver claramente el valor de i y total en cada iteración del bucle, lo cual es una forma poderosa de comprender el flujo del programa y encontrar errores.

Configuración del Comportamiento del Registro (Logging)

El módulo logging es altamente configurable. Puede controlar el nivel de severidad de los mensajes a mostrar, deshabilitar el registro por completo, o redirigir los mensajes de registro a un archivo en lugar de a la pantalla.

Niveles de Registro (Logging Levels)

Existen cinco niveles de registro estándar, cada uno con una función correspondiente:

Nivel Función de Registro Descripción
DEBUG logging.debug() Para detalles pequeños, útil solo al diagnosticar problemas.
INFO logging.info() Para registrar eventos generales o confirmar que las cosas funcionan como se espera.
WARNING logging.warning() Para indicar un problema potencial que no detiene el programa.
ERROR logging.error() Para registrar un error que causó el fallo de una operación específica.
CRITICAL logging.critical() Para un error fatal que está a punto de causar la terminación del programa.

Cuando establece un nivel de registro en basicConfig, Python mostrará los mensajes en ese nivel y en todos los niveles superiores. Por ejemplo, establecer level=logging.INFO mostrará mensajes INFO, WARNING, ERROR y CRITICAL, pero no DEBUG.

Deshabilitar el Registro

Después de la depuración, es posible que desee desactivar todos los mensajes de registro sin eliminar las llamadas a logging de su código. Puede hacerlo con logging.disable(). Acepta un nivel como argumento y deshabilita todos los mensajes de registro en ese nivel y por debajo. Por ejemplo, logging.disable(logging.CRITICAL) suprimirá todos los mensajes de registro.

Registro en un Archivo

En lugar de imprimir los registros en la consola, puede guardarlos en un archivo pasando el argumento de palabra clave filename a logging.basicConfig().

Practiquemos esto.

  1. Abra el archivo configure_logging.py.
  2. Agregue el siguiente código. Configura el registro para escribir todos los mensajes de nivel DEBUG y superior en un archivo llamado myProgramLog.txt.
import logging

## Configure logging to write to a file
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logging.debug('This is a debug message.')
logging.info('This is an info message.')
logging.warning('This is a warning message.')
  1. Ejecute el script desde la terminal.
python3 configure_logging.py

Esta vez, no verá ninguna salida en la terminal.

  1. Verifique el contenido del archivo myProgramLog.txt.
cat myProgramLog.txt

Verá que todos los mensajes de registro se han escrito en el archivo, completos con marcas de tiempo y niveles de severidad.

2023-10-27 10:30:00,123 - DEBUG - This is a debug message.
2023-10-27 10:30:00,123 - INFO - This is an info message.
2023-10-27 10:30:00,123 - WARNING - This is a warning message.

Esto concluye nuestro recorrido por las técnicas básicas de depuración en Python.

Resumen

En este laboratorio, ha aprendido y practicado varias técnicas esenciales de depuración en Python. Comenzó utilizando la sentencia raise para crear y activar excepciones personalizadas, y las manejó con bloques try...except. Luego aprendió a capturar un traceback (seguimiento de pila) completo de la excepción como una cadena de texto usando traceback.format_exc() para fines de registro (logging).

A continuación, exploró las sentencias assert como una forma de realizar verificaciones de cordura (sanity checks) y "fallar rápidamente" (fail fast) cuando su programa entra en un estado inesperado. Finalmente, profundizó en el módulo logging, aprendiendo a configurarlo para mostrar mensajes, controlar la salida con diferentes niveles de registro y escribir registros en un archivo para mantener registros persistentes.

Estas habilidades son fundamentales para escribir aplicaciones Python robustas, mantenibles y más fáciles de depurar.