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

Создайте собственные пользовательские классы исключений Python , адаптированные к вашему приложению.

Правильная обработка исключений позволяет легче читать код, но также требует от вас очень тщательно продумать то, что вы хотите сделать.

17 июня 2020
В Python
post main image
https://unsplash.com/@surface

Использование исключений в Python выглядит просто, но это не так. Наверное, перед написанием любого кода Python , но TL;DR. следует изучить работу с исключениями и обработку исключений. В интернете есть примеры, к сожалению, большинство из них очень тривиальны. Во всяком случае, я изучил это и придумал код, которым, как мне показалось, я с вами поделился. Оставьте комментарий, если у вас есть предложения.

Что такое ошибка и что такое исключение, в чем разница? В Python при возникновении ошибки во время выполнения программы возникает исключение, например, ValueError, ZeroDivisionError.

Преимущества исключений по сравнению с возвратом статуса можно обобщить следующим образом, см. также ссылку ниже "Почему лучше бросить исключение, чем возвращать код ошибки?

  1. Исключения оставляют ваш код чистым от всех проверок, необходимых при возврате статуса тестирования при каждом вызове.
  2. Исключения позволяют использовать возвращаемое функциями значение для действительных значений
  3. Самое главное: исключения нельзя игнорировать из-за бездействия, а возвраты статуса могут

Не все люди так позитивны, но я не собираюсь начинать здесь войну.

Где-то в нашем коде может быть обработчик исключений, который может быть использован, например, для отката вставки в базу данных. Также мы можем пустить пузырек исключения в верхнюю часть кода и представить ошибку user.

Протоколирование и параметры

Если что-то случится, мы должны убедиться, что заносим ошибку в журнал. Чтобы облегчить поиск и решение проблем (кодирования), мы помещаем как можно больше информации в наш журнал. Я решил, что мне нужна следующая информация в журнале:

  • обратная связь
  • следующие (необязательные) параметры:
    • e
      Это значение (часто) возвращается в виде исключения.

    • Это может быть наш собственный код ошибки

    • Это сообщение мы хотим показать user (посетитель сайта, API user)
    • подробности
      Больше информации, которая может быть полезна для user, например, поставляемые параметры
    • фарги
      Это аргументы функции, в которой произошло исключение, полезные для отладки

Пример

Предположим, что мы создаем приложение, которое использует класс базы данных и класс imap сервера. Мы создаем наш пользовательский обработчик исключений. Хорошей практикой является наличие его в файле exception.py, который мы импортируем в наше приложение:

# 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

Это очень похоже на создание обычных классов. В этом случае мы передаем ноль или более именованных аргументов в исключение. Нам нужен метод __str__(), чтобы Python возвращал строку с доступными данными, которая будет храниться в файле app.log. Если мы опустим __str__(), то app.log будет только показывать: exceptions.IMAPServerError".

Код приложения:

# 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()

Обратите внимание, что исключения записываются с помощью 'log.exception' в файл app.log. Также мы используем функцию Python locals() для перехвата аргументов функции или метода, в котором произошло исключение. В исключении 'connect to remote system' мы переподнимаем все параметры, которые нам нужны в журнале. Наконец, здесь мы используем конструкцию 'raise ... from e'. Это раскрывает ZeroDivisionError как изначальную причину.

Чтобы бежать, печатай:

python3 app.py

В результате:

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

и файл журнала 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 содержит две записи ОШИБКИ. Одна для ошибки 'Input must be 8 or 16' и одна для ошибки 'Connection to remote system failure'.

Резюме

Жизнь может быть простой, но это не так. Правильная обработка исключений дает вам легче читать код, но также требует от вас очень тщательно продумать то, что вы хотите сделать. Было бы неплохо, если бы мы также могли добавить аргументы вызываемых функций, ведущие к функции, поднимающей исключение, но это сложнее.

Ссылки / кредиты

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/

Подробнее

Exceptions

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.