Ciencia de datos: de la escuela al trabajo, Parte III

Introducción

Escribir código se trata de resolver problemas, pero no todos los problemas son predecibles. En el mundo real, su software encontrará situaciones inesperadas: archivos faltantes, entradas de usuarios no válidas, tiempos de espera de red o incluso fallas de hardware. Es por eso que manejar errores no es solo un agradable de tener; Es una parte crítica de la construcción de aplicaciones robustas y confiables para la producción.

Imagina un sitio web de comercio electrónico. Un cliente realiza un pedido, pero durante el proceso de pago, se produce un problema de conexión de la base de datos. Sin apropiado Manejo de erroreseste problema podría hacer que la aplicación se bloquee, dejando al cliente frustrado y la transacción incompleta. Peor aún, podría crear datos inconsistentes, lo que lleva a problemas aún mayores en el futuro. Por lo tanto, el manejo de errores es una habilidad fundamental para cualquier desarrollador de Python que quiera escribir código para la producción.

Sin embargo, el buen manejo de errores también va de la mano con un buen sistema de registro. Es raro tener acceso a la consola cuando el código se ejecuta en producción. Así que no hay posibilidad de que alguien se vea por alguien. Para asegurarse de que puede monitorear su aplicación e investigar cualquier incidente, debe configurar un sistema de registro. Aquí es donde el loguru El paquete entra en juego, que presentaré en este artículo.


Yo – ¿Cómo manejar los errores de Python?

En esta parte presento las mejores prácticas de manejo de errores en Pitónde los bloques Try-Except y el uso de raise hacia finally declaración. Estos conceptos lo ayudarán a escribir un código más limpio y mantenible que sea adecuado para un entorno de producción.

El tratar bloques

El bloque Try-Except es la herramienta principal para manejar errores en Python. Le permite detectar posibles errores durante la ejecución del código y evitar que el programa se bloquee.

def divide(a, b):
  try:
    return a / b
  except ZeroDivisionError:
    print(f"Only Chuck Norris can divide by 0!")

En esta función trivial, el bloque Try-Except permite que el error causado por una división por 0 sea interceptado. Se ejecuta el código en el bloque de try, y si se produce un error, el bloque excepto si es un ZeroDivisionError e imprima un mensaje. Pero solo se capta este tipo de error. Por ejemplo, si b es una cadena, se produce un error. Para evitar esto, puede agregar un TypeError. Por lo tanto, es importante probar todos los errores posibles.

La función se convierte en:

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print(f"Only Chuck Norris can divide by 0!")
    except TypeError:
        print("Do not compare apples and orange!")

Plantear una excepción

Puede usar la declaración de aumento para aumentar manualmente una excepción. Esto es útil si desea informar un error definido por el usuario o imponer una restricción específica en su código.

def divide(a, b):
    if b == 0:
        raise ValueError("Only Chuck Norris can divide by 0!")
    return a / b


try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")
except TypeError:
    print("Do not compare apples and orange!")

En este ejemplo, un ValueError Se activa la excepción si el divisor es cero. De esta manera, puede controlar explícitamente las condiciones de error. En la función de impresión, el mensaje será “Error: ¡solo Chuck Norris puede dividir por 0!“.

Algunas de las excepciones más comunes

ValueError: El tipo de valor es correcto pero su valor no es válido.

try:
    number = math.sqrt(-10)
except ValueError:
  print("It's too complex to be real!")

Keyerer: Intentando acceder a una clave que no existe en un diccionario.

data = {"name": "Alice"}
try:
    age = data["age"]
except KeyError:
    print("Never ask a lady her age!")

Índice: Intentando acceder a un índice inexistente en una lista.

items = [1, 2, 3]
try:
    print(items[3])
except IndexError:
    print("You forget that indexing starts at 0, don't you?")

TypeError: Realizar una operación en tipos incompatibles.

try:
    result = "text" + 5
except TypeError:
    print("Do not compare apples and orange!")

FilenotfoundError: Intentando abrir un archivo inexistente.

try:
    with open("notexisting_file.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("Are you sure of your path?")

Error personalizado: Puede activar excepciones predefinidas o también definir sus propias clases de excepción:

class CustomError(Exception):
    pass

try:
    raise CustomError("This is a custom error")
except CustomError as e:
    print(f"Catched error: {e}")

Limpiar con el finalmente declaración

El finally El bloque se ejecuta en todos los casos, independientemente de si el error ha ocurrido o no. A menudo se usa para realizar acciones de limpieza, como cerrar una conexión a una base de datos o liberar recursos.

import sqlite3

try:
    conn = sqlite3.connect("users_db.db")  # Connect to a database
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")  # Execute a query
    results = cursor.fetchall()  # Get result of the query
    print(results)
except sqlite3.DatabaseError as e:
    print("Database error:", e)
finally:
    print("Closing the database connection.")
    if 'conn' in locals():
        conn.close()  # Ensures the connection is closed

Las mejores prácticas para el manejo de errores

  1. Atrapar excepciones específicas: Evite usar un bloque genérico excepto sin especificar una excepción, ya que puede enmascarar errores inesperados. Prefiere especificar la excepción:
# Bad practice
try:
    result = 10 / 0
except Exception as e:
    print(f"Error: {e}")

# Good practice
try:
    result = 10 / 0
except ZeroDivisionError as e: 
    print(f"Error: {e}")
  1. Proporcionar mensajes explícitos: Agregue mensajes claros y descriptivos al elevar o manejar excepciones.
  2. Evite las fallas silenciosas: Si obtiene una excepción, asegúrese de registrar o volver a criar para que no pase desapercibido.
import logging

logging.basicConfig(level=logging.ERROR)

try:
    result = 10 / 0
except ZeroDivisionError:
    logging.error("Division by zero detected.")
  1. Usar else y finally bloques: El else el bloque se ejecuta solo si no se plantea ninguna excepción en el try bloquear.
try:
    result = 10 / 2
except ZeroDivisionError:
    logging.error("Division by zero detected.")
else:
    logging.info(f"Success: {result}")
finally:
    logging.info("End of processing.")

II – ¿Cómo manejar los registros de Python?

Un buen manejo de errores es una cosa, pero si nadie sabe que se ha producido un error, todo el punto se pierde. Como se explica en la introducción, el monitor rara vez se consulta o incluso se ve cuando un programa se está ejecutando en producción. Nadie verá impresión. Por lo tanto, el buen manejo de errores debe ir acompañado de un buen sistema de registro.

¿Qué son los registros?

Los registros son registros de mensajes generados por un programa para rastrear los eventos que ocurren durante su ejecución. Estos mensajes pueden contener información sobre errores, advertencias, acciones exitosas, hitos de proceso u otros eventos relevantes. Los registros son esenciales para depurar, rastrear el rendimiento y monitorear la salud de una aplicación. Permiten a los desarrolladores comprender lo que está sucediendo en un programa sin tener que interrumpir su ejecución, lo que hace que sea más fácil resolver problemas y mejorar continuamente el software.

El paquete Loguru

Python ya tiene un paquete de registro nativo: registro. Pero preferimos el paquete Loguru, que es mucho más simple de usar y más fácil de configurar. De hecho, el formato de salida completo ya está preconfigurado.

from loguru import logger
logger.debug("A pretty debug message!")
Imagen del autor.

Todos los elementos importantes se incluyen directamente en el mensaje:

  • Estampilla de tiempo
  • Nivel de registro, indicando la gravedad del mensaje.
  • Ubicación del archivo, módulo y número de línea. En este ejemplo, la ubicación del archivo es __main__ porque se ejecutó directamente desde la línea de comando. El módulo se debe debido al hecho de que el registro no está ubicado en una clase o función.
  • El mensaje.

Los diferentes niveles de registro

Hay varios niveles de registro a tener en cuenta la importancia del mensaje que se muestra (que es más complicado en una impresión). Cada nivel tiene un nombre y un número asociado:

  • RASTRO (5): Se utiliza para registrar información detallada sobre la ruta de ejecución del programa para fines de diagnóstico.
  • DEPURAR (10): Utilizado por los desarrolladores para grabar mensajes para fines de depuración.
  • Información (20): Se utiliza para registrar mensajes de información que describen la operación normal del programa.
  • ÉXITO (25): Similar a la información, pero se usa para indicar el éxito de una operación.
  • ADVERTENCIA (30): utilizado para indicar un evento inusual que puede requerir una mayor investigación.
  • ERROR (40): Se utiliza para registrar condiciones de error que han afectado una operación específica.
  • CRÍTICO (50): Se utiliza para registrar condiciones de error que evitan que una función principal funcione.

El paquete maneja naturalmente un formato diferente dependiendo del nivel utilizado

from loguru import logger

logger.trace("A trace message.")
logger.debug("A debug message.")
logger.info("An information message.")
logger.success("A success message.")
logger.warning("A warning message.")
logger.error("An error message.")
logger.critical("A critical message.")
Imagen del autor.

El mensaje de rastreo no se mostró porque el nivel mínimo predeterminado utilizado por Loguru es la depuración. Por lo tanto, ignora todos los mensajes en los niveles más bajos.

Es posible definir nuevos niveles de registro con el método de nivel y se utiliza con el método de registro

logger.level("FATAL", no=60, color="<red>", icon="!!!")
logger.log("FATAL", "A FATAL event has just occurred.")
  • Nombre: El nombre del nivel de registro.
  • No: el valor de gravedad correspondiente (debe ser un entero).
  • Color: marcado de color.
  • icono: el icono de nivel.

La configuración del registrador

Es posible recrear un registrador con una nueva configuración eliminando la anterior con el remove comando y generar un nuevo registrador con una nueva configuración con el add función. Esta función toma los siguientes argumentos:

  • hundir [mandatory]: Especifica un objetivo para cada conjunto de datos creado por el registrador. Por defecto, este valor se establece en sys.stderr (que corresponde a la salida de error estándar). También podemos almacenar toda la salida en un archivo “.log” (excepto si tiene un recopilador de registro).
  • nivel: Establece el nivel de registro mínimo para la grabadora.
  • formato: es útil para definir un formato personalizado para sus registros. Para mantener la coloración de los registros en el terminal, esto debe especificarse (ver el ejemplo a continuación).
  • filtrar: se usa para determinar si un registro debe registrarse o no.
  • colorear: toma un valor booleano y determina si el color terminal debe activarse o no.
  • fabricar en serie: hace que el registro se muestre en formato JSON si está configurado en True.
  • retroceso: Determina si el rastro de excepción debe ir más allá del punto en el que se registró el error para facilitar la resolución de problemas.
  • diagnosticar: Determina si los valores variables deben mostrarse en el rastro de excepción. Esta opción debe estar configurada en False en entornos de producción para que no se filtre información confidencial.
  • enoue: Si se activa esta opción, los registros de datos de registro se colocan en una cola para evitar conflictos si varios procesos se conectan al mismo objetivo.
  • atrapar: Si se produce un error inesperado cuando se conecta al sumidero especificado del servidor, puede detectarlo configurando esta opción en True. El error se mostrará en el error estándar.
import sys
from loguru import logger

logger_format = (
    "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
    "{level: <8} | "
    "{name}:{function}:{line}"
)
logger.remove()
logger.add(sys.stderr, format=logger_format)
    

Nota:
Los colores desaparecen en un archivo. Esto se debe a que hay caracteres especiales (llamados ansi Códigos) que muestran colores en la terminal, pero este formato no existe en los archivos.

Agregar contexto a los registros

Para aplicaciones complejas, puede ser útil agregar más información a los registros para habilitar la clasificación y facilitar la resolución de problemas.

Por ejemplo, si un usuario cambia una base de datos, puede ser útil tener la ID de usuario además de la información de cambio.

Antes de comenzar a grabar datos de contexto, debe asegurarse de que el {extra} La directiva se incluye en su formato personalizado. Esta variable es un diccionario de Python que contiene datos de contexto para cada entrada de registro (si corresponde).

Aquí hay un ejemplo de personalización donde un extra user_id se agrega. En este formato, los colores.

import sys
from loguru import logger


logger_format = (
    "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
    "<level>{level: <8}</level> | "
    "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
    "User ID: {extra[user_id]} - <level>{message}</level>"
)
logger.configure(extra={"user_id": ""})  # Default value
logger.remove()
logger.add(sys.stderr, format=logger_format)

Ahora es posible usar el método de enlace para crear un registrador infantil que herede todos los datos del registrador principal.

childLogger = logger.bind(user_id="001")
childLogger.info("Here a message from the child logger")

logger.info("Here a message from the parent logger")
Imagen del autor.

Otra forma de hacer esto es usar el método contextualizar en A With Block.

with logger.contextualize(user_id="001"):
    logger.info("Here a message from the logger with user_id 001")

logger.info("Here a message from the logger without user_id")
Imagen del autor.

En lugar del con bloque, puede usar un decorador. El código anterior se convierte en

@logger.contextualize(user_id="001")
def child_logger():
    logger.info("Here a message from the logger with user_id 001")

child_logger()

logger.info("Here a message from the logger without user_id")

El método de captura

Los errores se pueden registrar automáticamente cuando ocurren utilizando el método de captura.

def test(x):
    50/x

with logger.catch():
    test(0)
Imagen del autor.

Pero es más simple usar este método como decorador. Esto da como resultado el siguiente código

@logger.catch()
def test(x):
    50/x

test(0)

El archivo de registro

Una aplicación de producción está diseñada para ejecutarse continuamente e ininterrumpida. En algunos casos, es importante predecir el comportamiento del archivo; de lo contrario, tendrá que consultar páginas de registros en caso de un error.

Estas son las diferentes condiciones bajo las cuales se puede modificar un archivo:

  • rotación: Especifica una condición bajo la cual se cierra el archivo de registro actual y se crea un nuevo archivo. Esta condición puede ser un int, una fecha y hora o un str. Se recomienda STR ya que es más fácil de leer.
  • retención: Especifica cuánto tiempo se debe mantener cada archivo de registro antes de eliminarlo del sistema de archivos.
  • compresión: El archivo de registro se convierte en el formato de compresión especificado si esta opción está activada.
  • demora: Si esta opción se establece en True, la creación de un nuevo archivo de registro se retrasa hasta que se haya presionado el primer mensaje de registro.
  • modo, amortiguación, codificación : Parámetros que se pasan a la función de Python Abierto y determinan cómo Python abre archivos de registro.

Nota:
Por lo general, en el caso de una aplicación de producción, se configurará un colector de registros para recuperar las salidas de la aplicación directamente. Por lo tanto, no es necesario crear un archivo de registro.


Conclusión

El manejo de errores en Python es un paso importante para escribir código profesional y confiable. Al combinar los bloques Try-Except, la declaración de aumento y el bloque Finalmente, puede manejar errores previsiblemente mientras se mantiene el código legible y mantenible.

Además, un buen sistema de registro mejora la capacidad de monitorear y depurar su aplicación. Loguru proporciona un paquete simple y flexible para los mensajes de registro y, por lo tanto, se puede integrar fácilmente en su base de código.

En resumen, combinar el manejo de errores efectivos con un sistema de registro integral puede mejorar significativamente la capacidad de confiabilidad, mantenimiento y depuración de sus aplicaciones de Python.


Referencias

1 – Manejo de errores en Python: Documentación oficial de Python sobre excepciones

2 – La documentación del loguru: https://loguru.readthedocs.io/en/stable/

3 – Guía sobre loguru: https://betterstack.com/community/guides/logging/loguru/