Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)
En este tutorial, aprenderás a manejar las condiciones de error en Python desde el punto de vista del sistema completo. El manejo de errores es un aspecto fundamental del diseño, y va desde los niveles más bajos (a veces el hardware) hasta los usuarios finales. Si no tienes una estrategia coherente, tu sistema no será confiable, la experiencia del usuario será deficiente y tendrás muchos desafíos para depurar y solucionar problemas.
La clave del éxito es ser consciente de todos estos aspectos entrelazados, considerándolos explícitamente y formando una solución que resuelva 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 compatibilidad con el lenguaje y el tiempo de ejecución.
Python admite excepciones. Python y su biblioteca estándar usan excepciones libremente para informar sobre muchas situaciones excepcionales como errores de E/S, dividir por cero, indexar fuera de los límites y también algunas situaciones no tan excepcionales como el final de la iteración (aunque está oculta). La mayoría de las bibliotecas hacen lo mismo y generan excepciones.
Esto significa que tu código tendrá que controlar las excepciones generadas por Python y las bibliotecas de todos modos, por lo que también puede generar excepciones de tu código cuando sea necesario y no depender de los códigos de estado.
Ejemplo rápido
Antes de profundizar en el lugar sagrado de las excepciones de Python y las mejores prácticas de manejo de errores, veamos algunos manejos de excepciones en acción:
1 |
def f(): |
2 |
|
3 |
return 4 / 0 |
4 |
|
5 |
|
6 |
|
7 |
def g(): |
8 |
|
9 |
raise Exception("Don't call us. We'll call you") |
10 |
|
11 |
|
12 |
|
13 |
def h(): |
14 |
|
15 |
try: |
16 |
|
17 |
f() |
18 |
|
19 |
except Exception as e: |
20 |
|
21 |
print(e) |
22 |
|
23 |
try: |
24 |
|
25 |
g() |
26 |
|
27 |
except Exception as e: |
28 |
|
29 |
print(e) |
Esta es la salida al llamar a h():
1 |
h()
|
2 |
|
3 |
division by zero |
4 |
|
5 |
Don't call us. We'll call you
|
Excepciones de Python
Las excepciones de Python son objetos organizados en una jerarquía de clases.
Esta es toda la jerarquía:
1 |
BaseException |
2 |
|
3 |
+-- SystemExit |
4 |
|
5 |
+-- KeyboardInterrupt |
6 |
|
7 |
+-- GeneratorExit |
8 |
|
9 |
+-- Exception |
10 |
|
11 |
+-- StopIteration |
12 |
|
13 |
+-- StandardError |
14 |
|
15 |
| +-- BufferError |
16 |
|
17 |
| +-- ArithmeticError |
18 |
|
19 |
| | +-- FloatingPointError |
20 |
|
21 |
| | +-- OverflowError |
22 |
|
23 |
| | +-- ZeroDivisionError |
24 |
|
25 |
| +-- AssertionError |
26 |
|
27 |
| +-- AttributeError |
28 |
|
29 |
| +-- EnvironmentError |
30 |
|
31 |
| | +-- IOError |
32 |
|
33 |
| | +-- OSError |
34 |
|
35 |
| | +-- WindowsError (Windows) |
36 |
|
37 |
| | +-- VMSError (VMS) |
38 |
|
39 |
| +-- EOFError |
40 |
|
41 |
| +-- ImportError |
42 |
|
43 |
| +-- LookupError |
44 |
|
45 |
| | +-- IndexError |
46 |
|
47 |
| | +-- KeyError |
48 |
|
49 |
| +-- MemoryError |
50 |
|
51 |
| +-- NameError |
52 |
|
53 |
| | +-- UnboundLocalError |
54 |
|
55 |
| +-- ReferenceError |
56 |
|
57 |
| +-- RuntimeError |
58 |
|
59 |
| | +-- NotImplementedError |
60 |
|
61 |
| +-- SyntaxError |
62 |
|
63 |
| | +-- IndentationError |
64 |
|
65 |
| | +-- TabError |
66 |
|
67 |
| +-- SystemError |
68 |
|
69 |
| +-- TypeError |
70 |
|
71 |
| +-- ValueError |
72 |
|
73 |
| +-- UnicodeError |
74 |
|
75 |
| +-- UnicodeDecodeError |
76 |
|
77 |
| +-- UnicodeEncodeError |
78 |
|
79 |
| +-- UnicodeTranslateError |
80 |
|
81 |
+-- Warning |
82 |
|
83 |
+-- DeprecationWarning |
84 |
|
85 |
+-- PendingDeprecationWarning |
86 |
|
87 |
+-- RuntimeWarning |
88 |
|
89 |
+-- SyntaxWarning |
90 |
|
91 |
+-- UserWarning |
92 |
|
93 |
+-- FutureWarning |
94 |
|
95 |
+-- ImportWarning |
96 |
|
97 |
+-- UnicodeWarning |
98 |
|
99 |
+-- BytesWarning |
100 |
Hay varias excepciones especiales que se derivan directamente de BaseException, como SystemExit, KeyboardInterrupt y GeneratorExit. A continuación, está la clase Exception, que es la clase básica para StopIteration, StandardError y Warning. Todos los errores estándar se derivan de StandardError.
Cuando generas una excepción o alguna función que llamaste genera una excepción, ese flujo de código normal termina y la excepción comienza a propagarse por la pila de llamadas hasta que encuentra un controlador de excepciones adecuado. Si no hay ningún controlador de excepciones disponible para controlarlo, el proceso (o más exactamente el subproceso actual) terminará con un mensaje de excepción no controlado.
Generación de excepciones
Generar excepciones es muy fácil. Solo tienes que usar la palabra clave raise para generar un objeto que es una subclase de la clase Exception. Podría ser una instancia de Exception en sí, una de las excepciones estándar (por ejemplo, RuntimeError) o una subclase de Exception que tú mismo derivaste. Este es un pequeño fragmento de código que muestra todos los casos:
1 |
# Raise an instance of the Exception class itself
|
2 |
|
3 |
raise Exception('Ummm... something is wrong') |
4 |
|
5 |
|
6 |
|
7 |
# Raise an instance of the RuntimeError class
|
8 |
|
9 |
raise RuntimeError('Ummm... something is wrong') |
10 |
|
11 |
|
12 |
|
13 |
# Raise a custom subclass of Exception that keeps the timestamp the exception was created
|
14 |
|
15 |
from datetime import datetime |
16 |
|
17 |
|
18 |
|
19 |
class SuperError(Exception): |
20 |
|
21 |
def __init__(self, message): |
22 |
|
23 |
Exception.__init__(message) |
24 |
|
25 |
self.when = datetime.now() |
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
raise SuperError('Ummm... something is wrong') |
Captura de excepciones
Detectas excepciones con la cláusula except, como viste en el ejemplo. Cuando detectas una excepción, tienes tres opciones:
- Trágalo tranquilamente (manéjalo y sigue).
- Haz algo como el registro, pero vuelve a generar la misma excepción para permitir que los niveles más altos se controlen.
- Genera una excepción diferente en lugar del original.
Tragar la excepción
Debes tragarte la excepción si sabes cómo manejarla y puedes recuperarte por completo.
Por ejemplo, si recibes un archivo de entrada que puede estar en diferentes formatos (JSON, YAML), puedes intentar analizarlo con diferentes analizadores. Si el analizador JSON generó una excepción de que el archivo no es un archivo JSON válido, trágalo e intenta con el analizador YAML. Si el analizador YAML también falló, deja que la excepción se propague.
1 |
import json |
2 |
|
3 |
import yaml |
4 |
|
5 |
|
6 |
|
7 |
def parse_file(filename): |
8 |
|
9 |
try: |
10 |
|
11 |
return json.load(open(filename)) |
12 |
|
13 |
except json.JSONDecodeError |
14 |
|
15 |
return yaml.load(open(filename)) |
Ten en cuenta que otras excepciones (por ejemplo, el archivo no encontrado o sin permisos de lectura) se propagarán y no serán detectadas por la cláusula except específica. Esta es una buena política en este caso en el que quieres probar el análisis YAML solo si el análisis JSON falló debido a un problema de codificación JSON.
Si quieres manejar todas las excepciones, usa except Exception. Por ejemplo:
1 |
def print_exception_type(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
Ten en cuenta que al agregar as e, vinculas el objeto de excepción al nombre e disponible en tu cláusula except.
Volver a generar la misma excepción
Para volver a generar, simplemente agrega raise sin argumentos dentro de tu controlador. Esto te permite realizar un manejo local, pero aún permite que los niveles superiores también lo manejen. Aquí, la función invoke_function() imprime el tipo de excepción en la consola y luego vuelve a generar la excepción.
1 |
def invoke_function(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
10 |
|
11 |
raise
|
Generar una excepción diferente
Hay varios casos en los que te gustaría plantear una excepción diferente. A veces, quieres agrupar varias excepciones diferentes de bajo nivel en una sola categoría que se maneja uniformemente mediante un código de nivel superior. En los casos de orden, debes transformar la excepción al nivel de usuario y proporcionar un contexto específico de la aplicación.
Cláusula finally
A veces quieres asegurarte de que se ejecute algún código de limpieza incluso si se generó una excepción en algún momento. Por ejemplo, es posible que tengas una conexión a la base de datos que quieras cerrar una vez que hayas terminado. Esta es la forma incorrecta de hacerlo:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = open_db_connection() |
4 |
|
5 |
query(db) |
6 |
|
7 |
close_db_Connection(db) |
Si la función query() genera una excepción, la llamada a close_db_connection() nunca se ejecutará y la conexión de la base de datos permanecerá abierta. La cláusula finally siempre se ejecuta después de que se ejecuta el controlador de excepción try all. Así se hace correctamente:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = None |
4 |
|
5 |
try: |
6 |
|
7 |
db = open_db_connection() |
8 |
|
9 |
query(db) |
10 |
|
11 |
finally: |
12 |
|
13 |
if db is not None: |
14 |
|
15 |
close_db_connection(db) |
La llamada a open_db_connection() no puede devolver una conexión ni generar una excepción en sí. En este caso no hay necesidad de cerrar la conexión de la base de datos.
Al usar finally, debes tener cuidado de no generar ninguna excepción allí porque ocultará la excepción original.
Administradores de contexto
Los administradores de contexto proporcionan otro mecanismo para ajustar recursos como archivos o conexiones de base de datos en un código de limpieza que se ejecuta automáticamente incluso cuando se generaron excepciones. En lugar de bloques de prueba, usa la instrucción with. Este es un ejemplo con un archivo:
1 |
def process_file(filename): |
2 |
|
3 |
with open(filename) as f: |
4 |
|
5 |
process(f.read()) |
Ahora, incluso si process() genera una excepción, el archivo se cerrará correctamente inmediatamente cuando se salga del alcance del bloque with, independientemente de si la excepción se manejó o no.
Registro
El registro es prácticamente un requisito en sistemas no triviales y de larga duración. Es especialmente útil en aplicaciones web donde puedes tratar todas las excepciones de forma genérica: simplemente registra la excepción y devuelve un mensaje de error a la persona que llama.
Al iniciar sesión, es útil registrar el tipo de excepción, el mensaje de error y el stacktrace. Toda esta información está disponible a través del objeto sys.exc_info, pero si utilizas el método logger.exception() en tu controlador de excepciones, el sistema de registro de Python extraerá toda la información relevante para ti.
Esta es la mejor práctica que recomiendo:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
def f(): |
8 |
|
9 |
try: |
10 |
|
11 |
flaky_func() |
12 |
|
13 |
except Exception: |
14 |
|
15 |
logger.exception() |
16 |
|
17 |
raise
|
Si sigues este patrón entonces (suponiendo que configuraste el registro correctamente) pase lo que pase, tendrás un registro bastante bueno en tus registros de lo que salió mal, y podrás solucionar el problema.
Si vuelves a generar, asegúrate de no registrar la misma excepción una y otra vez en diferentes niveles. Es un desperdicio, y podría confundirte y hacerte creer 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 permitir que todas las excepciones se propaguen (a menos que se puedan manejar con confianza y tragar antes) y, luego, hacer el registro cerca del nivel superior de tu aplicación/sistema.
Sentry
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 siempre es la mejor solución.
Para realizar un seguimiento de las excepciones en toda tu infraestructura, un servicio como sentry es muy útil. Centraliza todos los informes de excepciones y, además del stacktrace, agrega el estado de cada marco de la pila (el valor de las variables en el momento en que se generó la excepción). También proporciona una interfaz muy agradable con paneles, informes y formas de desglosar los mensajes por varios proyectos. Es de código abierto, por lo que puedes ejecutar tu propio servidor o suscribirte a la versión alojada.
Lidiando con el fracaso transitorio
Algunos errores son temporales, en particular cuando se trata de sistemas distribuidos. Un sistema que se enloquece a la primera señal de problemas no es muy útil.
Si tu código accede 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 ajustar a medida que cambian las condiciones.
Otro enfoque es fallar rápidamente y luego volver a intentarlo. El beneficio es que si el objetivo está respondiendo rápido, entonces no tienes que pasar mucho tiempo en condiciones de sueño y puedes reaccionar inmediatamente. Pero si se produce un error, puedes reintentarlo varias veces hasta que decidas que es realmente inalcanzable y genera 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 @log_error, que registra una excepción y luego la vuelve a generar, y el decorador @retry, que volverá a intentar llamar a una función varias veces.
Registrador de errores
Esta es una implementación simple. El decorador excepto un objeto registrador. Cuando decora una función y se invoca la función, ajustará la llamada en una cláusula try-except y, si hubo una excepción, la registrará y, finalmente, volverá a generar la excepción.
1 |
def log_error(logger) |
2 |
|
3 |
def decorated(f): |
4 |
|
5 |
@functools.wraps(f) |
6 |
|
7 |
def wrapped(*args, **kwargs): |
8 |
|
9 |
try: |
10 |
|
11 |
return f(*args, **kwargs) |
12 |
|
13 |
except Exception as e: |
14 |
|
15 |
if logger: |
16 |
|
17 |
logger.exception(e) |
18 |
|
19 |
raise
|
20 |
|
21 |
return wrapped |
22 |
|
23 |
return decorated |
Así es como se usa:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
@log_error(logger) |
8 |
|
9 |
def f(): |
10 |
|
11 |
raise Exception('I am exceptional') |
Retrier
Esta es una muy buena implementación del decorador @retry.
1 |
import time |
2 |
|
3 |
import math |
4 |
|
5 |
|
6 |
|
7 |
# Retry decorator with exponential backoff
|
8 |
|
9 |
def retry(tries, delay=3, backoff=2): |
10 |
|
11 |
'''Retries a function or method until it returns True.
|
12 |
|
13 |
|
14 |
|
15 |
delay sets the initial delay in seconds, and backoff sets the factor by which
|
16 |
|
17 |
the delay should lengthen after each failure. backoff must be greater than 1,
|
18 |
|
19 |
or else it isn't really a backoff. tries must be at least 0, and delay
|
20 |
|
21 |
greater than 0.'''
|
22 |
|
23 |
|
24 |
|
25 |
if backoff <= 1: |
26 |
|
27 |
raise ValueError("backoff must be greater than 1") |
28 |
|
29 |
|
30 |
|
31 |
tries = math.floor(tries) |
32 |
|
33 |
if tries < 0: |
34 |
|
35 |
raise ValueError("tries must be 0 or greater") |
36 |
|
37 |
|
38 |
|
39 |
if delay <= 0: |
40 |
|
41 |
raise ValueError("delay must be greater than 0") |
42 |
|
43 |
|
44 |
|
45 |
def deco_retry(f): |
46 |
|
47 |
def f_retry(*args, **kwargs): |
48 |
|
49 |
mtries, mdelay = tries, delay # make mutable |
50 |
|
51 |
|
52 |
|
53 |
rv = f(*args, **kwargs) # first attempt |
54 |
|
55 |
while mtries > 0: |
56 |
|
57 |
if rv is True: # Done on success |
58 |
|
59 |
return True |
60 |
|
61 |
|
62 |
|
63 |
mtries -= 1 # consume an attempt |
64 |
|
65 |
time.sleep(mdelay) # wait... |
66 |
|
67 |
mdelay *= backoff # make future wait longer |
68 |
|
69 |
|
70 |
|
71 |
rv = f(*args, **kwargs) # Try again |
72 |
|
73 |
|
74 |
|
75 |
return False # Ran out of tries :-( |
76 |
|
77 |
|
78 |
|
79 |
return f_retry # true decorator -> decorated function |
80 |
|
81 |
return deco_retry # @retry(arg[, ...]) -> true decorator |
Conclusión
El manejo de errores es crucial tanto para los usuarios como para los desarrolladores. Python proporciona una gran compatibilidad en el lenguaje y la biblioteca estándar para el control de errores basado en excepciones. Si sigues las mejores prácticas cuidadosamente, puedes dominar este aspecto que suele ser ignorado.



