angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Cree sus propias clases de excepción Python personalizadas y adaptadas a su aplicación

El manejo adecuado de las excepciones te hace más fácil leer el código pero también requiere que pienses con mucho cuidado lo que quieres hacer.

17 junio 2020
En Python
post main image
https://unsplash.com/@surface

Usar excepciones en Python parece fácil, pero no lo es. Probablemente deberías estudiar las excepciones y el manejo de las mismas antes de escribir cualquier código Python excepto TL;DR. Hay ejemplos en Internet, desafortunadamente la mayoría son muy triviales. De todos modos, investigué esto y se me ocurrió un código que pensé en compartir con ustedes. Deje un comentario si tiene alguna sugerencia.

¿Qué es un error y qué es una excepción, cuál es la diferencia? En Python se plantea una excepción cuando se produce un error durante la ejecución del programa. El error puede ser, por ejemplo, un ValueError, ZeroDivisionError.

Las ventajas de las excepciones sobre la devolución de estatus pueden resumirse de la siguiente manera, véase también el enlace siguiente '¿Por qué es mejor lanzar una excepción que devolver un código de error?

  1. Exceptions deja su código limpio de todas las comprobaciones necesarias cuando se devuelve el estado de prueba en cada llamada
  2. Las excepciones permiten utilizar el valor de retorno de las funciones para los valores reales
  3. Lo más importante: las excepciones no pueden ser ignoradas por la inacción, mientras que los retornos de estado pueden

No todas las personas son tan positivas, pero no voy a empezar una guerra aquí.

En algún lugar de nuestro código podemos tener un manejador de excepciones que puede ser usado, por ejemplo, para deshacerse de una inserción en la base de datos. También podemos dejar que la excepción burbujee en la parte superior del código y presente un error en el user.

Registro y parámetros

Si algo sucede, debemos asegurarnos de registrar el error. Para facilitar la localización y resolución de problemas de codificación, ponemos en nuestro registro la mayor cantidad de información posible. Decidí que quería la siguiente información en el registro:

  • un rastreo
  • los siguientes parámetros (opcionales):
    • e
      Este es el valor (a menudo) devuelto por una excepción

    • Este puede ser nuestro propio código de error
    • mensaje
      Este es el mensaje que queremos mostrar al user (visitante de la página web, API user)
    • detalles
      Más información que puede ser útil para el user, por ejemplo, los parámetros suministrados

    • Estos son los argumentos de la función donde se produjo la excepción, útil para la depuración

Un ejemplo

Supongamos que creamos una aplicación que utiliza una clase de base de datos y una clase de servidor imap. Creamos nuestro manejador de excepciones personalizado. Es una buena práctica tenerlo en un archivo exceptions.py que importamos en nuestra aplicación:

# file: exceptions.py

class AppError(Exception):

    def __init__(self, e=None, code=None, message=None, details=None, fargs=None):
        self.e = e
        self.code = code
        self.message = message
        self.details = details
        self.fargs = fargs

    def get_e(self):
        return self.e

    def get_code(self):
        return self.code

    def get_message(self):
        return self.message

    def get_details(self):
        return self.details

    def __str__(self):
        s_items = []
        if self.e is not  None:
            s_items.append('e = {}'.format(self.e))
        if self.code is not  None:
            s_items.append('code = {}'.format(self.code))
        if self.message is not  None:
            s_items.append('message = {}'.format(self.message))
        if self.details is not  None:
            s_items.append('details = {}'.format(self.details))
        if self.fargs is not  None:
            s_items.append('fargs = {}'.format(self.fargs))
        return ', '.join(s_items)


class DbError(AppError):
    pass


class IMAPServerError(AppError):
    pass

Esto es muy parecido a crear clases normales. En este caso pasamos cero o más argumentos con nombre a la excepción. Necesitamos el método __str__() para que Python devuelva una cadena con los datos disponibles que se almacenarán en el archivo app.log. Si omitimos __str__() entonces sólo se mostrará app.log: "Excepciones.IMAPServerError".

El código de la aplicación:

# file: app.py
 
from exceptions import AppError, DbError, IMAPServerError

import logging

logging.basicConfig(
    format='%(asctime)s - %(levelname)s: %(message)s',
    filename='app.log',
    level=logging.DEBUG
    #level=logging.INFO
)

class A:

    def do_a(self, a):
        # connect to remote system
        b = B()
        b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })

        # do something else


class B:

    def do_b(self, a, b=None, c=None):
        fname = 'B.do_b'
        fargs = locals()

        abc = 'not in locals'

        # check values
        if b not in [8, 16]:
            raise IMAPServerError(
                fargs = fargs,
                e = 'Input must be 8 or 16',
                message = 'Input must be 8 or 16, value supplied: {}'.format(b)
            )

        # connect to remote system
        try:
            # simulate something went wrong
            # raise IMAPServerError('error 123')
            if b == 8:
                d = b/0

        except (ZeroDivisionError, IMAPServerError) as e:
            raise IMAPServerError(
                fargs = fargs,
                e = e,
                message = 'Connection to remote system failed. Please check your settings',
                details = 'Connection parameters:  username = John'
            ) from e


def run(i):

    a = A()
    a.do_a(i)


def do_run():

    # 7: input error
    # 8: connection error
    # 16: no error
    for i in [7, 8, 16]:

        print('Run with i = {}'.format(i))
        try:
            run(i)
            print('No error(s)')

        except (DbError, IMAPServerError) as e:
            logging.exception('Stack trace')
            print('Error: {}'.format(e.message))
            print('Details: {}'.format(e.details))

    print('Ready')


if __name__ == '__main__':
    do_run()

Tenga en cuenta que las excepciones se registran mediante "logging.exception" en un archivo app.log. También usamos la función Python locals() para capturar los argumentos de la función o método donde se produjo la excepción. En la excepción 'conectar con el sistema remoto', remontamos con todos los parámetros que queremos en el log. Finalmente, usamos la construcción 'raise ... from e' aquí. Esto revela el error de división cero como la causa original.

Para ejecutar, escribe:

python3 app.py

El resultado es:

Run with i = 7
Error: Input must be 8 or 16, value supplied: 7
Details:  None
Run with i = 8
Error: Connection to remote system failed. Please check your settings
Details: Connection parameters:  username = John
Run with i = 16
No error(s)
Ready

y el archivo de registro app.log:

2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 38, in do_b
    message = 'Input must be 8 or 16, value supplied: {}'.format(b)
exceptions.IMAPServerError: e = Input must be 8 or 16, message = Input must be 8 or 16, value supplied: 7, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 7, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}
2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 46, in do_b
    d = b/0
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 53, in do_b
    ) from e
exceptions.IMAPServerError: e = division by zero, message = Connection to remote system failed. Please check your settings, details = Connection parameters:  username = John, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 8, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}

app.log contiene dos entradas de ERROR. Una para el error "La entrada debe ser 8 o 16" y otra para el error "La conexión al sistema remoto falló".

Resumen

La vida podría ser simple pero no lo es. El manejo adecuado de las excepciones te hace más fácil leer el código pero también requiere que pienses con mucho cuidado lo que quieres hacer. Estaría bien si también pudiéramos añadir los argumentos de las funciones llamadas que conducen a la función que plantea la excepción, pero esto es más difícil.

Enlaces / créditos

How to get value of arguments passed to functions on the stack?
https://stackoverflow.com/questions/6061744/how-to-get-value-of-arguments-passed-to-functions-on-the-stack

Proper way to declare custom exceptions in modern Python?
https://stackoverflow.com/questions/1319615/proper-way-to-declare-custom-exceptions-in-modern-python

The Most Diabolical Python Antipattern
https://realpython.com/the-most-diabolical-python-antipattern/

Why Exceptions Suck (ckwop.me.uk)
https://news.ycombinator.com/item?id=232890

Why is it better to throw an exception rather than return an error code?
https://stackoverflow.com/questions/4670987/why-is-it-better-to-throw-an-exception-rather-than-return-an-error-code

Your API sucks (so I’ll catch all exceptions)
https://dorinlazar.ro/your-api-sucks-catch-exceptions/

Leer más

Exceptions

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.