Manejo profesional de errores con Python

En este tutorial, aprenderá a manejar las condiciones de error en Python desde un punto de vista completo del sistema. El manejo de errores es un aspecto crítico del diseño, y se extiende desde los niveles más bajos (a veces el hardware) hasta los usuarios finales. Si no tiene una estrategia consistente, su sistema no será confiable, la experiencia del usuario será deficiente y tendrá muchos desafíos en la depuración y la solución de problemas.. 

La clave del éxito es ser consciente de todos estos aspectos interconectados, considerarlos explícitamente y formar una solución que aborde cada punto..

Códigos de estado vs. excepciones

Hay dos modelos principales de manejo de errores: códigos de estado y excepciones. Los códigos de estado pueden ser utilizados por cualquier lenguaje de programación. Las excepciones requieren soporte de idioma / tiempo de ejecución. 

Python soporta excepciones. Python y su biblioteca estándar utilizan excepciones generosamente para informar sobre muchas situaciones excepcionales como los errores de IO, la división por cero, la indexación fuera de límites y también algunas situaciones no tan excepcionales como el final de la iteración (aunque está oculto). La mayoría de las bibliotecas siguen el ejemplo y plantean excepciones..

Eso significa que su código tendrá que manejar las excepciones generadas por Python y las bibliotecas de todos modos, por lo que también puede generar excepciones a su código cuando sea necesario y no confiar en los códigos de estado.

Ejemplo rápido

Antes de sumergirse en el santuario interno de Python, las excepciones y las mejores prácticas de manejo de errores, veamos algunas excepciones en la acción:

def f (): return 4/0 def g (): raise Exception ("No nos llame. Le llamaremos") def h (): try: f () excepto Exception como e: print (e) intente: g () excepto Excepción como e: imprimir (e)

Aquí está la salida al llamar h ():

h () división por cero No nos llames. Te llamaremos

Excepciones de Python

Las excepciones de Python son objetos organizados en una jerarquía de clases. 

Aquí está toda la jerarquía:

BaseException + - SystemExit + - KeyboardInterrupt + - GeneratorExit + - Exception + - StopIteration + - StandardError | + - BufferError | + - ArithmeticError | | + - FloatingPointError | | + - OverflowError | | + - ZeroDivisionError | + - AssertionError | + - AttributeError | + - EnvironmentError | | + - IOError | | + - OSError | | + - WindowsError (Windows) | | + - VMSError (VMS) | + - EOFError | + - ImportError | + - LookupError | | + - IndexError | | + - KeyError | + - MemoryError | + - NameError | | + - UnboundLocalError | + - ReferenceError | + - RuntimeError | | + - NotImplementedError | + - SyntaxError | | + - IndentationError | | + - TabError | + - SystemError | + - TypeError | + - ValueError | + - UnicodeError | + - UnicodeDecodeError | + - UnicodeEncodeError | + - UnicodeTranslateError + - Warning + - DeprecationWarning + - PendingDeprecationWarning + - RuntimeWarning + - SyntaxWarning + - UserWarning + - FutureWarning + - ImportWarning + - UnicodeWarning + - BytesWarning  

Hay varias excepciones especiales que se derivan directamente de BaseException, me gusta SystemExit, Teclado interrumpir y GeneratorExit. Luego está la Excepción clase, que es la clase base para StopIteration, Error estándar y Advertencia. Todos los errores estándar se derivan de Error estándar.

Cuando genera una excepción o alguna función a la que llama provoca una excepción, ese flujo de código normal termina y la excepción comienza a propagar la pila de llamadas hasta que encuentra un controlador de excepciones adecuado. Si no hay un controlador de excepciones disponible para manejarlo, el proceso (o más precisamente el subproceso actual) terminará con un mensaje de excepción no manejado.

Aumento de excepciones

Subir excepciones es muy fácil. Solo usas el aumento palabra clave para elevar un objeto que es una subclase de la Excepción clase. Podría ser una instancia de Excepción En sí, una de las excepciones estándar (por ejemplo,. Error de tiempo de ejecución), o una subclase de Excepción Te derivaste a ti mismo. Aquí hay un pequeño fragmento de código que demuestra todos los casos:

# Levante una instancia de la clase Exception. Aumente Exception ('Ummm ... algo está mal') # Levante una instancia de la clase RuntimeError la excepción se creó a partir de la clase de fecha y hora de dattime SuperError (Exception): def __init __ (self, message): Exception .__ init __ (message) self.when = datetime.now () sube SuperError ('Ummm ... algo está mal')

Atrapando excepciones

Se detectan excepciones con el excepto Cláusula, como viste en el ejemplo. Cuando detectas una excepción, tienes tres opciones:

  • Trágala tranquilamente (manéjala y sigue corriendo).
  • Haga algo como el registro, pero vuelva a elevar la misma excepción para permitir que los niveles más altos manejen.
  • Levante una excepción diferente en lugar del original.

Tragar la excepción

Debe tragar la excepción si sabe cómo manejarlo y puede recuperarse por completo.. 

Por ejemplo, si recibe un archivo de entrada que puede estar en diferentes formatos (JSON, YAML), puede intentar analizarlo utilizando diferentes analizadores. Si el analizador JSON generó una excepción de que el archivo no es un archivo JSON válido, tráguelo e intente con el analizador YAML. Si el analizador YAML falló también, dejas que la excepción se propague.

importar json importar yaml def parse_file (nombre de archivo): intentar: devolver json.load (abrir (nombre de archivo)) excepto json.JSONDecodeError devolver yaml.load (abrir (nombre de archivo))

Tenga en cuenta que otras excepciones (por ejemplo, el archivo no encontrado o los permisos de no lectura) se propagarán y no serán detectados por la cláusula de excepción específica. Esta es una buena política en este caso en el que desea probar el análisis de YAML solo si el análisis de JSON falló debido a un problema de codificación de JSON. 

Si quieres manejar todos excepciones entonces solo usa excepto excepción. Por ejemplo:

def print_exception_type (func, * args, ** kwargs): try: return func (* args, ** kwargs) excepto Exception como e: tipo de impresión (e)

Tenga en cuenta que al agregar como e, enlaza el objeto de excepción al nombre mi disponible en su cláusula de excepción.

Re-elevar la misma excepción

Para volver a subir, simplemente añada aumento sin argumentos dentro de su controlador. Esto le permite realizar un manejo local, pero también permite que los niveles superiores lo hagan también. Aquí el invoke_function () La función imprime el tipo de excepción en la consola y luego vuelve a elevar la excepción..

def invoke_function (func, * args, ** kwargs): try: return func (* args, ** kwargs) excepto Exception como e: print type (e) raise

Levantar una excepción diferente

Hay varios casos en los que desearía plantear una excepción diferente. A veces desea agrupar varias excepciones de bajo nivel diferentes en una sola categoría que se maneja de manera uniforme mediante un código de nivel superior. En casos de orden, necesita transformar la excepción al nivel de usuario y proporcionar algún contexto específico de la aplicación. 

Finalmente, cláusula

A veces desea asegurarse de que se ejecute algún código de limpieza incluso si se generó una excepción en algún momento. Por ejemplo, puede tener una conexión de base de datos que desea cerrar una vez que haya terminado. Aquí está la manera incorrecta de hacerlo:

def fetch_some_data (): db = open_db_connection () consulta (db) close_db_Connection (db)

Si el consulta() función plantea una excepción, entonces la llamada a close_db_connection () nunca se ejecutará y la conexión DB permanecerá abierta. los finalmente La cláusula siempre se ejecuta después de que se ejecute un controlador de excepciones. Aquí está cómo hacerlo correctamente:

def fetch_some_data (): db = None try: db = open_db_connection () consulta (db) finalmente: si db no es None: close_db_connection (db)

La llamada a open_db_connection () No puede devolver una conexión o provocar una excepción. En este caso no es necesario cerrar la conexión DB..

Cuando usas finalmente, tienes que tener cuidado de no generar excepciones allí porque enmascararán la excepción original.

Gestores de contexto

Los administradores de contexto proporcionan otro mecanismo para envolver recursos como archivos o conexiones de base de datos en un código de limpieza que se ejecuta automáticamente incluso cuando se han generado excepciones. En lugar de bloques try-finally, usas el con declaración. Aquí hay un ejemplo con un archivo:

def process_file (nombre de archivo): con abrir (nombre de archivo) como f: process (f.read ()) 

Ahora, incluso si proceso() provocó una excepción, el archivo se cerrará correctamente de inmediato cuando el alcance de la con se sale del bloque, independientemente de si la excepción se manejó o no.

Explotación florestal

El registro es prácticamente un requisito en sistemas no triviales y de larga ejecución. Es especialmente útil en aplicaciones web donde puede tratar todas las excepciones de forma genérica: simplemente registre la excepción y devuelva un mensaje de error a la persona que llama.. 

Cuando se registra, es útil registrar el tipo de excepción, el mensaje de error y el seguimiento de pila. Toda esta información está disponible a través del sys.exc_info objeto, pero si usas el logger.exception () En su controlador de excepciones, el sistema de registro de Python extraerá toda la información relevante para usted..

Esta es la mejor práctica que recomiendo:

importar logging logger = logging.getLogger () def f (): try: flaky_func () excepto Exception: logger.exception () raise

Si sigue este patrón, entonces (suponiendo que configure el registro correctamente) no importa lo que pase, tendrá un registro bastante bueno en sus registros de lo que salió mal y podrá solucionar el problema..

Si vuelve a aumentar, asegúrese de no registrar la misma excepción una y otra vez en diferentes niveles. Es un desperdicio, y podría confundirlo y hacerle pensar que se produjeron varias instancias del mismo problema, cuando en la práctica se registró una sola instancia varias veces.

La forma más sencilla de hacerlo es dejar que todas las excepciones se propaguen (a menos que puedan manejarse con confianza y ser tragadas antes) y luego hacer el registro cerca del nivel superior de su aplicación / sistema..

Centinela

El registro es una capacidad. La implementación más común es el uso de archivos de registro. Pero, para sistemas distribuidos a gran escala con cientos, miles o más servidores, esta no es siempre la mejor solución. 

Para hacer un seguimiento de las excepciones en toda su infraestructura, un servicio como centinela es muy útil. Centraliza todos los informes de excepciones y, además del apilamiento, agrega el estado de cada marco de pila (el valor de las variables en el momento en que se generó la excepción). También proporciona una interfaz realmente agradable con paneles, informes y formas de desglosar los mensajes por múltiples proyectos. Es de código abierto, por lo que puede ejecutar su propio servidor o suscribirse a la versión alojada..

Tratar con el fracaso transitorio

Algunos fallos son temporales, en particular cuando se trata de sistemas distribuidos. Un sistema que se asusta a la primera señal de problemas no es muy útil. 

Si su código está accediendo a algún sistema remoto que no responde, la solución tradicional son los tiempos de espera, pero a veces no todos los sistemas están diseñados con tiempos de espera. Los tiempos de espera no siempre son fáciles de calibrar a medida que cambian las condiciones. 

Otro enfoque es fallar rápido y luego volver a intentarlo. El beneficio es que si el objetivo responde rápidamente, no tiene que pasar mucho tiempo en la condición de sueño y puede reaccionar de inmediato. Pero si falló, puede volver a intentarlo varias veces hasta que decida que es realmente inalcanzable y generar una excepción. En la siguiente sección, presentaré un decorador que puede hacerlo por ti..

Decoradores útiles

Dos decoradores que pueden ayudar con el manejo de errores son los @log_error, que registra una excepción y luego la vuelve a elevar, y la @procesar de nuevo decorador, que volverá a intentar llamar a una función varias veces.

Registrador de errores

Aquí hay una implementación simple. El decorador exceptúa un objeto logger. Cuando decora una función y se invoca la función, envolverá la llamada en una cláusula try-except, y si hubiera una excepción, la registrará y finalmente volverá a subir la excepción..

def log_error (logger) def decorado (f): @ functools.wraps (f) def envuelto (* args, ** kwargs): try: return f (* args, ** kwargs) excepto Exception como e: if logger: logger .excepción (e) elevar retorno envuelto retorno decorado

Aquí está cómo usarlo:

importar logging logger = logging.getLogger () @log_error (logger) def f (): aumentar Excepción ('Soy excepcional')

Retrier

Aquí hay una muy buena implementación del decorador @retry..

tiempo de importación importar matemática # Reintentar decorador con retroceso exponencial def reintento (intentos, retardo = 3, retroceso = 2): "reintenta una función o método hasta que devuelve Verdadero. El retardo establece el retardo inicial en segundos, y el retroceso establece el factor por el el retraso debe prolongarse después de cada falla. El retroceso debe ser mayor que 1, o de lo contrario no es realmente un retroceso. Los intentos deben ser al menos 0 y el retraso mayor que 0. "si el retroceso <= 1: raise ValueError("backoff must be greater than 1") tries = math.floor(tries) if tries < 0: raise ValueError("tries must be 0 or greater") if delay <= 0: raise ValueError("delay must be greater than 0") def deco_retry(f): def f_retry(*args, **kwargs): mtries, mdelay = tries, delay # make mutable rv = f(*args, **kwargs) # first attempt while mtries > 0: si rv es Verdadero: # Hecho en retorno exitoso mtries Verdaderos - = 1 # consume un intento time.sleep (mdelay) # espera ... mdelay * = retroceso # hace que el futuro espere más tiempo rv = f (* args, ** kwargs) # Intentar nuevamente devolver Falso # Se agotó el intento :-( devolver f_retry # verdadero decorador -> función decorada devolver deco_retry # @retry (arg [, ...]) -> verdadero decorador

Conclusión

El manejo de errores es crucial tanto para los usuarios como para los desarrolladores. Python proporciona una gran compatibilidad en el idioma y en la biblioteca estándar para el manejo de errores basado en excepciones. Al seguir diligentemente las mejores prácticas, puede conquistar este aspecto a menudo descuidado.

Aprender Python

Aprende Python con nuestra completa guía de tutoriales de Python, ya sea que estés empezando o seas un programador experimentado que busca aprender nuevas habilidades..