Aaron Maxwell es autor de Powerful Python.
Las excepciones ocurren. Y como desarrolladores, simplemente tenemos que lidiar con ellas. Incluso cuando escribimos software para ayudarnos a encontrar burritos.
Espera, me estoy adelantando… ya volveremos a ello. Como decía: La forma de tratar las excepciones depende del lenguaje. Y para el software que opera a escala, el registro es una de las herramientas más poderosas y valiosas que tenemos para tratar las condiciones de error. Veamos algunas de las formas en que estos trabajan juntos.
El Patrón de la «Gran Lona»
Vamos a empezar en un extremo:
try: main_loop()except Exception: logger.exception("Fatal error in main loop")
Este es un amplio cajón de sastre. Es adecuado para alguna ruta de código en la que sabes que el bloque de código (es decir, main_loop()
) puede levantar una serie de excepciones que no puedes anticipar. Y en lugar de permitir que el programa termine, usted decide que es preferible registrar la información de error, y continuar desde allí.
La magia aquí es con el método exception
. (logger
es el objeto logger de su aplicación-algo que fue devuelto desde logging.getLogger()
, por ejemplo). Este maravilloso método captura el seguimiento completo de la pila en el contexto del bloque de excepción, y lo escribe en su totalidad.
Nota que no tienes que pasar el objeto de excepción aquí. Lo que hay que hacer es pasar una cadena de mensajes. Esto registrará la traza completa de la pila, pero antepondrá una línea con su mensaje. Así que el mensaje de varias líneas que se muestra en su registro podría ser como este:
Fatal error in main loopTraceback (most recent call last): File "bigtarp.py", line 14, in main_loop() File "bigtarp.py", line 9, in main_loop print(foo(x)) File "bigtarp.py", line 4, in foo return 10 // n ZeroDivisionError: integer division or modulo by zero
Los detalles del seguimiento de la pila no importan-este es un ejemplo de juguete que ilustra una solución adulta a un problema del mundo real. Sólo observe que la primera línea es el mensaje que pasó a logger.exception()
, y las líneas siguientes son el seguimiento completo de la pila, incluyendo el tipo de excepción (ZeroDivisionError en este caso). Atrapará y registrará cualquier tipo de error de esta manera.
Por defecto, logger.exception
utiliza el nivel de registro de ERROR. Alternativamente, puede utilizar los métodos regulares de registro – logger.debug()
, logger.info()
, logger.warn()
, etc. – y pasar el parámetro exc_info
, poniéndolo a True:
while True: try: main_loop() except Exception: logger.error("Fatal error in main loop", exc_info=True)
Poner exc_info
a True hará que el registro incluya el seguimiento completo de la pila…. exactamente como lo hace logger.exception()
. La única diferencia es que puede cambiar fácilmente el nivel de registro a algo distinto de error: Sólo tienes que sustituir logger.error
por logger.warn
, por ejemplo.
Dato divertido: El patrón Big Tarp tiene una contrapartida casi diabólica, que leerás a continuación.
El patrón «Pinpoint»
Ahora veamos el otro extremo. Imagina que estás trabajando con el SDK de OpenBurrito, una biblioteca que resuelve el problema crucial de encontrar un local de burritos nocturno cerca de tu ubicación actual. Supongamos que tiene una función llamada find_burrito_joints()
que normalmente devuelve una lista de restaurantes adecuados. Pero bajo ciertas circunstancias raras, puede lanzar una excepción llamada BurritoCriteriaConflict.
from openburrito import find_burrito_joints, BurritoCriteriaConflict# "criteria" is an object defining the kind of burritos you want.try: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) places = list()
El patrón aquí es ejecutar optimistamente algún código-la llamada a find_burrito_joints()
, en este caso-dentro de un bloque try. En el caso de que se produzca un tipo de excepción específico, se registra un mensaje, se trata la situación y se sigue adelante.
La diferencia clave es la cláusula except. Con Big Tarp, básicamente estás atrapando y registrando cualquier excepción posible. Con Pinpoint, estás atrapando un tipo de excepción muy específico, que tiene relevancia semántica en ese lugar particular del código.
Nota también, que uso logger.warn()
en lugar de logger.exception()
. (En este artículo, dondequiera que veas warn()
, puedes sustituirlo por info()
, o error()
, etc.) En otras palabras, registro un mensaje en una gravedad particular en lugar de registrar todo el stack trace.
¿Por qué estoy tirando la información del stack trace? Porque no es tan útil en este contexto, donde estoy capturando un tipo de excepción específico, que tiene un significado claro en la lógica del código. Por ejemplo, en este fragmento:
characters = {"hero": "Homer", "villain": "Mr. Burns"}# Insert some code here that may or may not add a key called# "sidekick" to the characters dictionary.try: sidekick = charactersexcept KeyError: sidekick = "Milhouse"
Aquí, el KeyError
no es un error cualquiera. Cuando se produce, significa que ha ocurrido una situación específica, es decir, que no hay un rol de «compañero» definido en mi elenco de personajes, por lo que debo recurrir a uno por defecto. Llenar el registro con un seguimiento de la pila no va a ser útil en este tipo de situación. Y ahí es donde usarás Pinpoint.
El Patrón «Transformador»
Aquí, estás atrapando una excepción, registrándola, y luego lanzando una excepción diferente. Primero, así es como funciona en Python 3:
try: something()except SomeError as err: logger.warn("...") raise DifferentError() from err
En Python 2, debes dejar de lado el «from err»:
try: something()except SomeError as err: logger.warn("...") raise DifferentError()
(Esto tiene grandes implicaciones. Más sobre esto en un momento.) Querrás usar este patrón cuando se levante una excepción que no se adapte bien a la lógica de tu aplicación. Esto ocurre a menudo alrededor de los límites de la biblioteca.
Por ejemplo, imagina que estás usando el SDK openburrito
para tu aplicación asesina que permite a la gente encontrar locales de burritos por la noche. La función find_burrito_joints()
puede levantar BurritoCriteriaConflict
si somos demasiado exigentes. Esta es la API expuesta por el SDK, pero no se corresponde convenientemente con la lógica de alto nivel de su aplicación. Un mejor ajuste en este punto del código es una excepción que definiste, llamada NoMatchingRestaurants
.
En esta situación, aplicarás el patrón así (para Python 3):
from myexceptions import NoMatchingRestaurantstry: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) raise NoMatchingRestaurants(criteria) from err
Esto causa una sola línea de salida en tu registro, y dispara una nueva excepción. Si nunca se captura, la salida de error de esa excepción se ve así:
Traceback (most recent call last): File "transformerB3.py", line 8, in places = find_burrito_joints(criteria) File "/Users/amax/python-exception-logging-patterns/openburrito.py", line 7, in find_burrito_joints raise BurritoCriteriaConflictopenburrito.BurritoCriteriaConflictThe above exception was the direct cause of the following exception:Traceback (most recent call last): File "transformerB3.py", line 11, in raise NoMatchingRestaurants(criteria) from errmyexceptions.NoMatchingRestaurants: {'region': 'Chiapas'}
Ahora esto es interesante. La salida incluye el seguimiento de la pila para NoMatchingRestaurants
. Y también informa del BurritoCriteriaConflict
instigador… especificando claramente cuál fue el original.
En Python 3, las excepciones ahora se pueden encadenar. La sintaxis raise ... from ...
proporciona esto. Cuando dices levantar NoMatchingRestaurants(criteria)
desde err, eso levanta una excepción de typeNoMatchingRestaurants
. Esta excepción levantada tiene un atributo llamado __cause__
, cuyo valor es la excepción instigadora. Python 3 hace uso de esto internamente cuando reporta la información del error.
¿Cómo se hace esto en Python 2? Pues no se puede. Esta es una de esas bondades que tienes que actualizar para obtener. En Python 2, la sintaxis «raise … from» no está soportada, por lo que tu salida de la excepción incluirá sólo el stack trace de NoMatchingRestaurants
. El patrón Transformer sigue siendo perfectamente útil, por supuesto.
El patrón «Message and Raise»
En este patrón, registras que una excepción ocurre en un punto particular, pero luego permites que se propague y sea manejada en un nivel superior:
try: something()except SomeError: logger.warn("...") raise
No estás manejando realmente la excepción. Sólo estás interrumpiendo temporalmente el flujo para registrar un evento. Usted hará esto cuando usted tiene específicamente un controlador de nivel superior para el error, y quiere recurrir a eso, pero también quiere registrar que el error se produjo, o el significado de la misma, en un determinado lugar en el código.
Esto puede ser más útil en la solución de problemas-cuando usted está recibiendo una excepción, pero tratando de entender mejor el contexto de llamada. Puede interponer esta declaración de registro para proporcionar información útil, e incluso desplegar con seguridad a la producción si necesita observar los resultados en condiciones realistas.
El Antipatrón «Mensaje críptico»
Ahora vamos a centrar nuestra atención en algunos antipatrones… cosas que no debe hacer en su código.
try: something()except Exception: logger.error("...")
Supongamos que usted o alguien de su equipo escribe este código, y luego, seis meses más tarde, ve un mensaje curioso en su registro. Algo como:
ERROR: something bad happened
Ahora espero y rezo para que no veas las palabras «algo malo sucedió» en tu registro real. Sin embargo, el texto del registro real que ve puede ser igual de desconcertante. ¿Qué debe hacer ahora?
Bueno, lo primero es averiguar en qué parte del código base se está registrando este vago mensaje. Si tienes suerte, serás capaz de buscar rápidamente en el código y encontrar exactamente una posibilidad. Si no tiene suerte, puede encontrar el mensaje de registro en varias rutas de código completamente diferentes. Lo que le dejará con varias preguntas:
- ¿Cuál de ellas está provocando el error?
- ¿O son varias de ellas? O TODOS ellos?
- ¿Qué entrada en el registro corresponde a qué lugar?
A veces, sin embargo, ni siquiera es posible grep o buscar a través del código para encontrar donde está sucediendo, porque el texto del registro se está generando. Considera:
what = "something" quality = "bad" event = "happened" logger.error("%s %s %s", what, quality, event)
¿Cómo buscarías eso? Es posible que ni siquiera se le ocurra, a menos que busque la frase completa primero y no obtenga resultados. Y si obtuvieras un resultado, podría ser fácilmente un falso positivo.
La solución ideal es pasar el argumento exc_info
:
try: something()except Exception: logger.error("something bad happened", exc_info=True)
Cuando haces esto, se incluye un seguimiento completo de la pila en los registros de la aplicación. Esto te dice exactamente qué línea en qué archivo está causando el problema, quién lo invocó, etc… toda la información que necesitas para empezar a depurar.
El antipatrón más diabólico de Python
Si alguna vez te veo hacer esto, iré a tu casa a confiscar tus ordenadores, luego hackearé tu cuenta de github y borraré todos tus repos:
try: something()except Exception: pass
En mi boletín, me refiero a esto como «El antipatrón más diabólico de Python». Observe cómo esto no sólo no le da ninguna información útil sobre la excepción. También se las arregla para ocultar completamente el hecho de que algo está mal en primer lugar. Puede que nunca sepas que has escrito mal el nombre de una variable -sí, esto enmascara el NameError- hasta que te llamen a las 2 de la mañana porque la producción se ha roto, de una manera que te llevará hasta el amanecer para averiguarlo, porque toda la información posible para solucionar el problema está siendo suprimida.
Simplemente no lo hagas. Si sientes que simplemente debes atrapar e ignorar todos los errores, al menos pon una gran lona debajo (es decir, usa logger.exception()
en lugar de pass
).
Más patrones de registro de excepciones
Hay muchos más patrones para registrar información de excepciones en Python, con diferentes compensaciones, pros y contras. Qué patrones has encontrado útiles, o no? Hágalo saber en los comentarios.
Aprenda más sobre los fundamentos del registro en Python.