Manejo de errores y excepciones

Beginner

This tutorial is from open-source community. Access the source code

Introducción

Aunque las excepciones se introdujeron anteriormente, esta sección completa algunos detalles adicionales sobre la comprobación de errores y el manejo de excepciones.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 85%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Cómo fallan los programas

Python no realiza ninguna comprobación o validación de los tipos o valores de los argumentos de las funciones. Una función funcionará con cualquier dato que sea compatible con las instrucciones de la función.

def add(x, y):
    return x + y

add(3, 4)               ## 7
add('Hello', 'World')   ## 'HelloWorld'
add('3', '4')           ## '34'

Si hay errores en una función, aparecen en tiempo de ejecución (como una excepción).

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>

Para verificar el código, se da una gran importancia a las pruebas (que se abordan más adelante).

Excepciones

Las excepciones se utilizan para señalar errores. Para generar una excepción por tu cuenta, utiliza la instrucción raise.

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

Para capturar una excepción, utiliza try-except.

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

Manejo de Excepciones

Las excepciones se propagan hasta el primer except que coincide.

def grok():
  ...
    raise RuntimeError('Whoa!')   ## Se lanza la excepción aquí

def spam():
    grok()                        ## Llamada que causará la excepción

def bar():
    try:
       spam()
    except RuntimeError as e:     ## Se captura la excepción aquí
      ...

def foo():
    try:
         bar()
    except RuntimeError as e:     ## La excepción no llega aquí
      ...

foo()

Para manejar la excepción, coloca las instrucciones en el bloque except. Puedes agregar cualquier instrucción que desees para manejar el error.

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## Se captura la excepción aquí
        instrucciones           ## Utiliza estas instrucciones
        instrucciones
      ...

bar()

Después de manejar la excepción, la ejecución continúa con la primera instrucción después del try-except.

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## Se captura la excepción aquí
        instrucciones
        instrucciones
      ...
    instrucciones                  ## Continúa la ejecución aquí
    instrucciones                  ## Y sigue aquí
  ...

bar()

Excepciones Integradas

Hay aproximadamente dos docenas de excepciones integradas. Por lo general, el nombre de la excepción indica lo que está mal (por ejemplo, se lanza una ValueError porque se proporcionó un valor incorrecto). Esta no es una lista exhaustiva. Consulte la documentación para obtener más información.

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

Valores de Excepción

Las excepciones tienen un valor asociado. Contiene información más específica sobre lo que está mal.

raise RuntimeError('Invalid user name')

Este valor es parte de la instancia de excepción que se coloca en la variable suministrada a except.

try:
 ...
except RuntimeError as e:   ## `e` contiene la excepción generada
 ...

e es una instancia del tipo de excepción. Sin embargo, a menudo parece una cadena cuando se imprime.

except RuntimeError as e:
    print('Failed : Reason', e)

Capturando múltiples errores

Puedes capturar diferentes tipos de excepciones utilizando múltiples bloques except.

try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
except KeyboardInterrupt as e:
...

Alternativamente, si las instrucciones para manejarlas son las mismas, puedes agruparlas:

try:
...
except (IOError,LookupError,RuntimeError) as e:
...

Capturando todos los errores

Para capturar cualquier excepción, use Exception de la siguiente manera:

try:
 ...
except Exception:       ## PELIGRO. Ver abajo
    print('An error occurred')

En general, escribir código así es una mala idea porque no tendrás idea de por qué falló.

Manera incorrecta de capturar errores

A continuación se muestra la manera incorrecta de utilizar excepciones.

try:
    go_do_something()
except Exception:
    print('Computer says no')

Esto captura todos los posibles errores y puede hacer imposible el depuración cuando el código falla por alguna razón que no esperabas en absoluto (por ejemplo, un módulo de Python desinstalado, etc.).

Enfoque un poco mejor

Si vas a capturar todos los errores, este es un enfoque más razonable.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

Reporta una razón específica de falla. Casi siempre es una buena idea tener algún mecanismo para ver/reportar errores cuando escribes código que captura todas las posibles excepciones.

En general, sin embargo, es mejor capturar el error tan específicamente como sea razonable. Solo captura los errores que puedas realmente manejar. Deja que otros errores pasen - quizás algún otro código los pueda manejar.

Re-lanzando una excepción

Utiliza raise para propagar un error capturado.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

Esto te permite tomar medidas (por ejemplo, registrar) y pasar el error al llamador.

Mejores prácticas para excepciones

No captures excepciones. Fallo rápido y con ruido. Si es importante, alguien más se ocupará del problema. Solo captura una excepción si eres ese alguien. Es decir, solo captura errores donde puedas recuperarte y seguir adelante de manera razonable.

Sentencia finally

Especifica código que debe ejecutarse independientemente de si se produce una excepción o no.

lock = Lock()
...
lock.acquire()
try:
  ...
finally:
    lock.release()  ## esto SIEMPRE se ejecutará. Con o sin excepción.

Comúnmente se utiliza para administrar de manera segura los recursos (especialmente los candados, archivos, etc.).

Sentencia with

En el código moderno, try-finally a menudo se reemplaza con la sentencia with.

lock = Lock()
with lock:
    ## lock adquirido
  ...
## lock liberado

Un ejemplo más familiar:

with open(filename) as f:
    ## Utilizar el archivo
  ...
## Archivo cerrado

with define un contexto de uso para un recurso. Cuando la ejecución sale de ese contexto, los recursos se liberan. with solo funciona con ciertos objetos que han sido programados específicamente para soportarlo.

Ejercicio 3.8: Lanzamiento de excepciones

La función parse_csv() que escribiste en la última sección permite la selección de columnas especificadas por el usuario, pero eso solo funciona si el archivo de datos de entrada tiene encabezados de columna.

Modifica el código para que se lance una excepción si se pasan ambos argumentos select y has_headers=False. Por ejemplo:

>>> parse_csv('/home/labex/project/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

Luego de agregar esta comprobación, podrías preguntarte si deberías realizar otros tipos de comprobaciones de validez en la función. Por ejemplo, ¿deberías comprobar que el nombre de archivo sea una cadena, que tipos sea una lista o algo por el estilo?

Como regla general, por lo general es mejor omitir esas pruebas y simplemente dejar que el programa falle con entradas incorrectas. El mensaje de traza apuntará a la fuente del problema y puede ayudar en la depuración.

La principal razón para agregar la comprobación anterior es evitar ejecutar el código en un modo sin sentido (por ejemplo, usar una característica que requiere encabezados de columna, pero al mismo tiempo especificar que no hay encabezados).

Esto indica un error de programación en el código llamador. Comprobar casos que "no deben suceder" a menudo es una buena idea.

Ejercicio 3.9: Captura de excepciones

La función parse_csv() que escribiste se utiliza para procesar todo el contenido de un archivo. Sin embargo, en el mundo real, es posible que los archivos de entrada tengan datos dañados, faltantes o sucios. Intenta este experimento:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

Modifica la función parse_csv() para capturar todas las excepciones ValueError generadas durante la creación de registros y mostrar un mensaje de advertencia para las filas que no se pueden convertir.

El mensaje debe incluir el número de fila y la información sobre el motivo por el cual falló. Para probar tu función, intenta leer el archivo missing.csv de arriba. Por ejemplo:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Fila 4: No se pudo convertir ['MSFT', '', '51.23']
Fila 4: Razón literal no válida para int() con base 10: ''
Fila 7: No se pudo convertir ['IBM', '', '70.44']
Fila 7: Razón literal no válida para int() con base 10: ''
>>>
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

Ejercicio 3.10: Silenciar errores

Modifica la función parse_csv() para que los mensajes de error de análisis se puedan silenciar si el usuario lo desea explícitamente. Por ejemplo:

>>> portfolio = parse_csv('missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

El manejo de errores es una de las cosas más difíciles de hacer bien en la mayoría de los programas. Como regla general, no debes ignorar silenciosamente los errores. En cambio, es mejor informar sobre los problemas y dar a los usuarios la opción de silenciar el mensaje de error si lo eligen.

Resumen

¡Felicitaciones! Has completado el laboratorio de comprobación de errores. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.