Меню

Python exception как получить текст ошибки

In this Python Tutorial let us learn about the 3 different pieces of information that you can extract and use from the Exceptions caught on your except clauses, and see the best ways to use each of these pieces in our Python programs.

Let us start by learning what the 3 pieces of information are.

What kind of information can you get from Exceptions?

You can get the following 3 pieces of data from exceptions

  • Exception Type,
  • Exception Value a.k.a Error Message, and
  • Stack-trace or Traceback Object.

All three of the above information is printed out by the Python Interpreter when our program crashes due to an exception as shown in the following example

>> my_list = [1,2]
>> print (my_list[3])
Traceback (most recent call last):

  File "<ipython-input-35-63c7f9106be5>", line 1, in <module>
    print (my_list[3])

IndexError: list index out of range

Lines 3,4,5,6 shows the Stack-trace
Line 7 shows the Exception type and Error Message.

Our focus in this article is to learn how to extract the above 3 pieces individually in our except clauses and print them out as needed.

Hence, the rest of the article is all about answering the following questions

  • what does each of the information in the above list mean,
  • how to extract each of these 3 pieces individually and
  • how to use these pieces in our programs.

Piece#1: Printing Exception Type

The Exception Type refers to the class to which the Exception that you have just caught belongs to.

Extracting Piece#1 (Exception Type)

Let us improve our Example 1 above by putting the problematic code into try and except clauses.

try:
  my_list = [1,2]
  print (my_list[3])
except Exception as e:
  print(type(e))

Here, in the try clause, we have declared a List named my_list and initialized the list with 2 items. Then we have tried to print the 3rd/non-existent item in the list.

The except clause catches the IndexError exception and prints out Exception type.

On running the code, we will get the following output

<class 'IndexError'>

As you can see we just extracted and printed out the information about the class to which the exception that we have just caught belongs to!

But how exactly did we do that?
If you have a look at the except clause. In the line

except Exception as e:

what we have done is, we have assigned the caught exception to an object named “e”. Then by using the built-in python function type(), we have printed out the class name that the object e belongs to.

print(type(e))

Where to get more details about Exception Types

Now that we have the “Exception Type”, next we will probably need to get some information about that particular type so that we can get a better understanding of why our code has failed. In order to do that, the best place to start is the official documentation.

For built in exceptions you can have a look at the Python Documentation

For Exception types that come with the libraries that you use with your code, refer to the documentation of your library.

Piece#2: Printing Exception Value a.k.a Error message

The Exception type is definitely useful for debugging, but, a message like IndexError might sound cryptic and a good understandable error-message will make our job of debugging easier without having to look at the documentation.

In other words, if your program is to be run on the command line and you wish to log why the program just crashed then it is better to use an “Error message” rather than an “Exception Type”.

The example below shows how to print such an Error message.

try:
  my_list = [1,2]
  print (my_list[3])
except Exception as e:
  print(e)

This will print the default error message as follows

list index out of range

Each Exception type comes with its own error message. This can be retrieved using the built-in function print().

Say your program is going to be run by a not-so-tech-savvy user, in that case, you might want to print something friendlier. You can do so by passing in the string to be printed along with the constructor as follows.

try:
  raise IndexError('Custom message about IndexError')
except Exception as e:
  print(e)

This will print

Custom message about IndexError

To understand how the built-in function print() does this magic, and see some more examples of manipulating these error messages, I recommend reading my other article in the link below.

Python Exception Tutorial: Printing Error Messages (5 Examples!)

If you wish to print both the Error message and the Exception type, which I recommend, you can do so like below.

try:
  my_list = [1,2]
  print (my_list[3])
except Exception as e:
  print(repr(e))

This will print something like

IndexError('list index out of range')

Now that we have understood how to get control over the usage of Pieces 1 and 2, let us go ahead and look at the last and most important piece for debugging, the stack-trace which tells us where exactly in our program have the Exception occurred.

Piece#3: Printing/Logging the stack-trace using the traceback object

Stack-trace in Python is packed into an object named traceback object.

This is an interesting one as the traceback class in Python comes with several useful methods to exercise complete control over what is printed.

Let us see how to use these options using some examples!

import traceback

try:
  my_list = [1,2]
  print (my_list[3])
except Exception:
  traceback.print_exc()

This will print something like

Traceback (most recent call last):
  File "<ipython-input-38-f9a1ee2cf77a>", line 5, in <module>
    print (my_list[3])
IndexError: list index out of range

which contains the entire error messages printed by the Python interpreter if we fail to handle the exception.

Here, instead of crashing the program, we have printed this entire message using our exception handler with the help of the print_exc() method of the traceback class.

The above Example-6 is too simple, as, in the real-world, we will normally have several nested function calls which will result in a deeper stack. Let us see an example of how to control the output in such situations.

def func3():
  my_list = [1,2]
  print (my_list[3])

def func2():
  print('calling func3')
  func3()

def func1():
  print('calling func2')
  func2()

try:
  print('calling func1')
  func1()
except Exception as e:
    traceback.print_exc()

Here in the try clause we call func1(), which in-turn calls func2(), which in-turn calls func3(), which produces an IndexError. Running the code above we get the following output

calling func1
calling func2
calling func3
Traceback (most recent call last):
  File "<ipython-input-42-2267707e164f>", line 15, in <module>
    func1()
  File "<ipython-input-42-2267707e164f>", line 11, in func1
    func2()
  File "<ipython-input-42-2267707e164f>", line 7, in func2
    func3()
  File "<ipython-input-42-2267707e164f>", line 3, in func3
    print (my_list[3])
IndexError: list index out of range

Say we are not interested in some of the above information. Say we just want to print out the Traceback and skip the error message and Exception type (the last line above), then we can modify the code like shown below.

def func3():
  my_list = [1,2]
  print (my_list[3])

def func2():
  func3()

def func1():
  func2()

try:
  func1()
except Exception as e:
    traceback_lines = traceback.format_exc().splitlines()
    for line in traceback_lines:
      if line != traceback_lines[-1]:
        print(line)

Here we have used the format_exc() method available in the traceback class to get the traceback information as a string and used splitlines() method to transform the string into a list of lines and stored that in a list object named traceback_lines

Then with the help of a simple for loop we have skipped printing the last line with index of -1 to get an output like below

Traceback (most recent call last):
  File "<ipython-input-43-aff649563444>", line 3, in <module>
    func1()
  File "<ipython-input-42-2267707e164f>", line 11, in func1
    func2()
  File "<ipython-input-42-2267707e164f>", line 7, in func2
    func3()
  File "<ipython-input-42-2267707e164f>", line 3, in func3
    print (my_list[3])

Another interesting variant of formatting the information in the traceback object is to control the depth of stack that is actually printed out.

If your program uses lots of external library code, odds are the stack will get very deep, and printing out each and every level of function call might not be very useful. If you ever find yourself in such a situation you can set the limit argument in the print_exc() method like shown below.

traceback.print_exc(limit=2, file=sys.stdout)

This will limit the number of levels to 2. Let us use this line of code in our Example and see how that behaves

def func3():
  my_list = [1,2]
  print (my_list[3])

def func2():
  func3()

def func1():
  func2()

try:
  func1()
except Exception as e:
  traceback.print_exc(limit=2)

This will print

Traceback (most recent call last):
  File "<ipython-input-44-496132ff4faa>", line 12, in <module>
    func1()
  File "<ipython-input-44-496132ff4faa>", line 9, in func1
    func2()
IndexError: list index out of range

As you can see, we have printed only 2 levels of the stack and skipped the 3rd one, just as we wanted!

You can do more things with traceback like formatting the output to suit your needs. If you are interested to learn even more things to do, refer to the official documentation on traceback here

Now that we have seen how to exercise control over what gets printed and how to format them, next let us have a look at some best practices on when to use which piece of information

Best Practices while Printing Exception messages

When to Use Which Piece

  • Use Piece#1 only on very short programs and only during the development/testing phase to get some clues on the Exceptions without letting the interpreter crash your program. Once finding out, implement specific handlers to do something about these exceptions. If you are not sure how to handle the exceptions have a look at my other article below where I have explained 3 ways to handle Exceptions
    Exceptions in Python: Everything You Need To Know!
  • Use Piece#2 to print out some friendly information either for yourself or for your user to inform them what exactly is happening.
  • Use all 3 pieces on your finished product, so that if an exception ever occurs while your program is running on your client’s computer, you can log the errors and have use that information to fix your bugs.

Where to print

One point worth noting here is that the default file that print() uses is the stdout file stream and not the stderr stream. To use stderr instead, you can modify the code like this

import sys
try:
  #some naughty statements that irritate us with exceptions
except Exception as e:
  print(e, file=sys.stderr)

The above code is considered better practice, as errors are meant to go to stderr and not stdout.

You can always print these into a separate log file too if that is what you need. This way, you can organize the logs collected in a better manner by separating the informational printouts from the error printouts.

How to print into log files

If you are going to use a log file I suggest using python’s logging module instead of print() statements, as described here

If you are interested in learning how to manually raise exceptions, and what situations you might need to do that you can read this article below

Python: Manually throw/raise an Exception using the “raise” statement

If you are interested in catching and handling multiple exception in a single except clause, you can this article below

Python: 3 Ways to Catch Multiple Exceptions in a single “except” clause

And with that, I will conclude this article!
I hope you enjoyed reading this article and got some value out of it!
Feel free to share it with your friends and colleagues!

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

Исключения в языках программирования

Исключениями (exceptions) в языках программирования называют проблемы, возникающие в ходе выполнения программы, которые допускают возможность дальнейшей ее работы в рамках основного алгоритма. Типичным примером исключения является деление на ноль, невозможность считать данные из файла (устройства), отсутствие доступной памяти, доступ к закрытой области памяти и т.п. Для обработки таких ситуаций в языках программирования, как правило, предусматривается специальный механизм, который называется обработка исключений (exception handling).

Исключения разделяют на синхронные и асинхронные. Синхронные исключения могут возникнуть только в определенных местах программы. Например, если у вас есть код, который открывает файл и считывает из него данные, то исключение типа “ошибка чтения данных” может произойти только в указанном куске кода. Асинхронные исключения могут возникнуть в любой момент работы программы, они, как правило, связаны с какими-либо аппаратными проблемами, либо приходом данных. В качестве примера можно привести сигнал отключения питания.

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

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

В Python выделяют два различных вида ошибок: синтаксические ошибки и исключения.

Синтаксические ошибки в Python

Синтаксические ошибки возникают в случае если программа написана с нарушениями требований Python к синтаксису. Определяются они в процессе парсинга программы. Ниже представлен пример с ошибочным написанием функции print.

>>> for i in range(10):
    prin("hello!")

Traceback (most recent call last):
  File "<pyshell#2>", line 2, in <module>
    prin("hello!")
NameError: name 'prin' is not defined

Исключения в Python

Второй вид ошибок – это исключения. Они возникают в случае если синтаксически программа корректна, но в процессе выполнения возникает ошибка (деление на ноль и т.п.). Более подробно про понятие исключения написано выше, в разделе “исключения в языках программирования”.

Пример исключения ZeroDivisionError, которое возникает при делении на 0.

>>> a = 10
>>> b = 0
>>> c = a / b
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    c = a / b
ZeroDivisionError: division by zero

В Python исключения являются определенным типом данных, через который пользователь (программист) получает информацию об ошибке. Если в коде программы исключение не обрабатывается, то приложение останавливается и в консоли печатается подробное описание произошедшей ошибки с указанием места в программе, где она произошла и тип этой ошибки.

Иерархия исключений в Python

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

BaseException
+– SystemExit
+– KeyboardInterrupt
+– GeneratorExit
+– Exception
     +– StopIteration
     +– StopAsyncIteration
     +– ArithmeticError
     |    +– FloatingPointError
     |    +– OverflowError
     |    +– ZeroDivisionError
     +– AssertionError
     +– AttributeError
     +– BufferError
     +– EOFError
     +– ImportError
          +– ModuleNotFoundError
     +– LookupError
     |    +– IndexError
     |    +– KeyError
     +– MemoryError
     +– NameError
     |    +– UnboundLocalError
     +– OSError
     |    +– BlockingIOError
     |    +– ChildProcessError
     |    +– ConnectionError
     |    |    +– BrokenPipeError
     |    |    +– ConnectionAbortedError
     |    |    +– ConnectionRefusedError
     |    |    +– ConnectionResetError
     |    +– FileExistsError
     |    +– FileNotFoundError
     |    +– InterruptedError
     |    +– IsADirectoryError
     |    +– NotADirectoryError
     |    +– PermissionError
     |    +– ProcessLookupError
     |    +– TimeoutError
     +– ReferenceError
     +– RuntimeError
     |    +– NotImplementedError
     |    +– RecursionError
     +– SyntaxError
     |    +– IndentationError
     |         +– TabError
     +– SystemError
     +– TypeError
     +– ValueError
     |    +– UnicodeError
     |         +– UnicodeDecodeError
     |         +– UnicodeEncodeError
     |         +– UnicodeTranslateError
     +– Warning
          +– DeprecationWarning
          +– PendingDeprecationWarning
          +– RuntimeWarning
          +– SyntaxWarning
          +– UserWarning
          +– FutureWarning
          +– ImportWarning
          +– UnicodeWarning
          +– BytesWarning
          +– ResourceWarning

Как видно из приведенной выше схемы, все исключения являются подклассом исключения BaseException. Более подробно об иерархии исключений и их описании можете прочитать здесь.

Обработка исключений в Python

Обработка исключений нужна для того, чтобы приложение не завершалось аварийно каждый раз, когда возникает исключение. Для этого блок кода, в котором возможно появление исключительной ситуации необходимо поместить во внутрь синтаксической конструкции try…except.

print("start")
try:
   val = int(input("input number: "))
   tmp = 10 / val
   print(tmp)
except Exception as e:
   print("Error! " + str(e))
print("stop")

В приведенной выше программе возможных два вида исключений – это ValueError, возникающее в случае, если на запрос программы “введите число”, вы введете строку, и ZeroDivisionError – если вы введете в качестве числа 0.

Вывод программы при вводе нулевого числа будет таким.

start input number: 0 Error! stop

Если бы инструкций try…except не было, то при выбросе любого из исключений программа аварийно завершится.

print("start")
val = int(input(“input number: “))
tmp = 10 / val
print(tmp)
print("stop")

Если ввести 0 на запрос приведенной выше программы, произойдет ее остановка с распечаткой сообщения об исключении.

start


input number: 0


Traceback (most recent call last):


 File “F:/work/programming/python/devpractice/tmp.py”, line 3, in <module>


   tmp = 10 / val


ZeroDivisionError: division by zero

Обратите внимание, надпись stop уже не печатается в конце вывода программы.

Согласно документу по языку Python, описывающему ошибки и исключения, оператор try работает следующим образом:

  • Вначале выполняется код, находящийся между операторами try и except.
  • Если в ходе его выполнения исключения не произошло, то код в блоке except пропускается, а код в блоке try выполняется весь до конца.
  • Если исключение происходит, то выполнение в рамках блока try прерывается и выполняется код в блоке except. При этом для оператора except можно указать, какие исключения можно обрабатывать в нем. При возникновении исключения, ищется именно тот блок except, который может обработать данное исключение.
  • Если среди except блоков нет подходящего для обработки исключения, то оно передается наружу из блока try. В случае, если обработчик исключения так и не будет найден, то исключение будет необработанным (unhandled exception) и программа аварийно остановится.

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

Если бы мы в нашей программе хотели обрабатывать только ValueError и ZeroDivisionError, то программа выглядела бы так.

print("start")
try:
   val = int(input("input number: "))
   tmp = 10 / val
   print(tmp)
except(ValueError, ZeroDivisionError):
   print("Error!")
print("stop")

Или так, если хотим обрабатывать ValueError, ZeroDivisionError по отдельность, и, при этом, сохранить работоспособность при возникновении исключений отличных от вышеперечисленных.

print("start")
try:
   val = int(input("input number: "))
   tmp = 10 / val
   print(tmp)
except ValueError:
   print("ValueError!")
except ZeroDivisionError:
   print("ZeroDivisionError!")
except:
   print("Error!")
print("stop")

Существует возможность передать подробную информацию о произошедшем исключении в код внутри блока except.

rint("start")
try:
   val = int(input("input number: "))
   tmp = 10 / val
   print(tmp)
except ValueError as ve:
   print("ValueError! {0}".format(ve))
except ZeroDivisionError as zde:
   print("ZeroDivisionError! {0}".format(zde))
except Exception as ex:
   print("Error! {0}".format(ex))
print("stop")

Использование finally в обработке исключений

Для выполнения определенного программного кода при выходе из блока try/except, используйте оператор finally.

try:
   val = int(input("input number: "))
   tmp = 10 / val
   print(tmp)
except:
   print("Exception")
finally:
  print("Finally code")

Не зависимо от того, возникнет или нет во время выполнения кода в блоке try исключение, код в блоке finally все равно будет выполнен.

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

try:
   f = open("tmp.txt", "r")
   for line in f:
       print(line)
   f.close()
except Exception as e:
   print(e)
else:
   print("File was readed")

Генерация исключений в Python

Для принудительной генерации исключения используется инструкция raise.

Самый простой пример работы с raise может выглядеть так.

try:
   raise Exception("Some exception")
except Exception as e:
   print("Exception exception " + str(e))

Таким образом, можно “вручную” вызывать исключения при необходимости.

Пользовательские исключения (User-defined Exceptions) в Python

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

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

class NegValException(Exception):
   pass

try:
   val = int(input("input positive number: "))
   if val < 0:
       raise NegValException("Neg val: " + str(val))
   print(val + 10)
except NegValException as e:
  print(e)

P.S.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

<<< Python. Урок 10. Функции в Python   Python. Урок 12. Ввод-вывод данных. Работа с файлами>>>

Одним из недостатков гибких языков, таких как Python, является предположение, что если что-то работает, то скорее всего оно сделано правильно. Я хочу написать скромное руководство по эффективному использованию исключений в Python, правильной их обработке и логировании.

Эффективная обработка исключений

Введение

Давайте рассмотрим следующую систему. У нас есть микросервис, который отвечает за:

·  Прослушивание событий о новом заказе;

·  Получение заказа из базы данных;

·  Проверку состояния принтера;

·  Печать квитанции;

·  Отправка квитанции в налоговую систему (IRS).

В любой момент может сломаться что угодно. У вас могут возникнуть проблемы с объектом заказа, в котором может не быть нужной информации, или в принтере может закончиться бумага, или же сервис налоговой не будет работать, и вы не сможете синхронизировать с ними квитанцию об оплате, а может быть ваша база данных окажется недоступна.

Ваша задача правильно и проактивно реагировать на любую ситуацию, чтобы избежать ошибок при обработке новых заказов.

И примерно вот такой код на этот случай пишут люди (он, конечно, работает, но плохо и неэффективно):

class OrderService:
    def emit(self, order_id: str) -> dict:

        try:
            order_status = status_service.get_order_status(order_id)
        except Exception as e:
            logger.exception(
                f"Order {order_id} was not found in db "
                f"to emit. Error: {e}."
            )
            raise e

        (
            is_order_locked_in_emission,
            seconds_in_emission,
        ) = status_service.is_order_locked_in_emission(order_id)
        if is_order_locked_in_emission:
            logger.info(
                "Redoing emission because "
                "it was locked in that state after a long time! "
                f"Time spent in that state: {seconds_in_emission} seconds "
                f"Order: {order_id}, "
                f"order_status: {order_status.value}"
            )

        elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
            logger.info("Aborting emission request because it is already in progress!")
            return {"order_id": order_id, "order_status": order_status.value}

        elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
            logger.info(
                "Aborting emission because it already happened! "
                f"Order: {order_id}, "
                f"order_status: {order_status.value}"
            )
            return {"order_id": order_id, "order_status": order_status.value}

        try:
            receipt_note = receipt_service.create(order_id)
        except Exception as e:
            logger.exception(
                "Error found during emission! "
                f"Order: {order_id}, "
                f"exception: {e}"
            )
            raise e

        try:
            broker.emit_receipt_note(receipt_note)
        except Exception as e:
            logger.exception(
                "Emission failed! "
                f"Order: {order_id}, "
                f"exception: {e}"
            )
            raise e

        order_status = status_service.get_order_status(order_id)
        return {"order_id": order_id, "order_status": order_status.value}

Сначала я сосредоточусь на том, что OrderService слишком много знает, и все эти данные делают его чем-то вроде blob, а чуть позже расскажу о правильной обработке и правильном логировании исключений.

Почему этот сервис — blob?

Этот сервис знает слишком много. Кто-то может сказать, что он знает только то, что ему нужно (то есть все шаги, связанные с формированием чека), но на самом деле он знает куда больше.

Он сосредоточен на создании ошибок (например, база данных, печать, статус заказа), а не на том, что он делает (например, извлекает, проверяет статус, генерирует, отправляет) и на том, как следует реагировать в случае сбоев.

 В этом смысле мне кажется, что клиент учит сервис тому, какие исключения он может выдать. Если мы решим переиспользовать его на любом другом этапе (например, клиент захочет получить еще одну печатную копию более старого чека по заказу), мы скопируем большую часть этого кода.

Несмотря на то, что сервис работает нормально, поддерживать его трудно, и неясно, как один шаг соотносится с другим из-за повторяющихся блоков except между шагами, которые отвлекают наше внимание на вопрос «как» вместо того, чтобы думать о «когда».

Первое улучшение: делайте исключения конкретными

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

Я выделю только то, что мы поменяли:

try:
    order_status = status_service.get_order_status(order_id)
except Exception as e:
    logger.exception(...)
    raise OrderNotFound(order_id) from e

...

try:
    ...
except Exception as e:
    logger.exception(...)
    raise ReceiptGenerationFailed(order_id) from e

try:
    broker.emit_receipt_note(receipt_note)
except Exception as e:
    logger.exception(...)
    raise ReceiptEmissionFailed(order_id) from e

Обратите внимание, что на этот раз я пользуюсь from e, что является правильным способом создания одного исключения из другого и сохраняет полную трассировку стека.

Второе улучшение: не лезьте не в свое дело

Теперь, когда у нас есть кастомные исключения, мы можем перейти к мысли «не учите классы тому, что может пойти не так» — они сами скажут, если это случится!

# Services

class StatusService:
    def get_order_status(order_id):
        try:
            ...
        except Exception as e:
            raise OrderNotFound(order_id) from e


class ReceiptService:
    def create(order_id):
        try:
            ...
        except Exception as e:
            raise ReceiptGenerationFailed(order_id) from e


class Broker:
    def emit_receipt_note(receipt_note):
        try:
            ...
        except Exception as e:
            raise ReceiptEmissionFailed(order_id) from e

# Main class

class OrderService:
    def emit(self, order_id: str) -> dict:
        try:
            order_status = status_service.get_order_status(order_id)

            (
                is_order_locked_in_emission,
                seconds_in_emission,
            ) = status_service.is_order_locked_in_emission(order_id)
            if is_order_locked_in_emission:
                logger.info(
                    "Redoing emission because "
                    "it was locked in that state after a long time! "
                    f"Time spent in that state: {seconds_in_emission} seconds "
                    f"Order: {order_id}, "
                    f"order_status: {order_status.value}"
                )

            elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
                logger.info("Aborting emission request because it is already in progress!")
                return {"order_id": order_id, "order_status": order_status.value}

            elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
                logger.info(
                    "Aborting emission because it already happened! "
                    f"Order: {order_id}, "
                    f"order_status: {order_status.value}"
                )
                return {"order_id": order_id, "order_status": order_status.value}

            receipt_note = receipt_service.create(order_id)
            broker.emit_receipt_note(receipt_note)
            order_status = status_service.get_order_status(order_id)
        except OrderNotFound as e:
            logger.exception(
                f"Order {order_id} was not found in db "
                f"to emit. Error: {e}."
            )
            raise
        except ReceiptGenerationFailed as e:
            logger.exception(
                "Error found during emission! "
                f"Order: {order_id}, "
                f"exception: {e}"
            )
            raise
        except ReceiptEmissionFailed as e:
            logger.exception(
                "Emission failed! "
                f"Order: {order_id}, "
                f"exception: {e}"
            )
            raise
        else:
            return {"order_id": order_id, "order_status": order_status.value}

Как вам? Намного лучше, правда? У нас есть один блок try, который построен достаточно логично, чтобы понять, что произойдет дальше. Вы сгруппировали конкретные блоки, за исключением тех, которые помогают вам понять «когда» и крайние случаи. И, наконец, у вас есть блок else, в котором описано, что произойдет, если все отработает как надо.

Кроме того, пожалуйста, обратите внимание на то, что я сохранил инструкции raise без объявления объекта исключения. Это не опечатка. На самом деле, это правильный способ повторного вызова исключения: простой и немногословный.

Но это еще не все. Логирование продолжает меня раздражать.

Третье улучшение: улучшение логирования

Этот шаг напоминает мне принцип «говори, а не спрашивай», хотя это все же не совсем он. Вместо того, чтобы запрашивать подробности исключения и на их основе выдавать полезные сообщения, исключения должны выдавать их сами – в конце концов, я их конкретизировал!

### Exceptions

class OrderCreationException(Exception):
    pass


class OrderNotFound(OrderCreationException):
    def __init__(self, order_id):
        self.order_id = order_id
        super().__init__(
            f"Order {order_id} was not found in db "
            "to emit."
        )


class ReceiptGenerationFailed(OrderCreationException):
    def __init__(self, order_id):
        self.order_id = order_id
        super().__init__(
            "Error found during emission! "
            f"Order: {order_id}"
        )


class ReceiptEmissionFailed(OrderCreationException):
    def __init__(self, order_id):
        self.order_id = order_id
        super().__init__(
            "Emission failed! "
            f"Order: {order_id} "
        )

### Main class

class OrderService:
    def emit(self, order_id: str) -> dict:
        try:
            ...
        except OrderNotFound:
            logger.exception("We got a database exception")
            raise
        except ReceiptGenerationFailed:
            logger.exception("We got a problem generating the receipt")
            raise
        except ReceiptEmissionFailed:
            logger.exception("Unable to emit the receipt")
            raise
        else:
            return {"order_id": order_id, "order_status": order_status.value}

Наконец-то мои глаза чувствуют облегчение. Поменьше повторений, пожалуйста! Примите к сведению, что рекомендуемый способ выглядит так, как я написал его выше: logger.exception(«ЛЮБОЕ СООБЩЕНИЕ»). Вам даже не нужно передавать исключение, поскольку его наличие уже подразумевается. Кроме того, кастомное сообщение, которое мы определили в каждом исключении с идентификатором order_id, будет отображаться в логах, поэтому вам не нужно повторяться и не нужно оперировать внутренними данными об исключениях.

Вот пример вывода ваших логов:

❯ python3 testme.py
Unable to emit the receipt # <<-- My log message
Traceback (most recent call last):
  File "/path/testme.py", line 19, in <module>
    tryme()
  File "/path/testme.py", line 14, in tryme
    raise ReceiptEmissionFailed(order_id)
ReceiptEmissionFailed: Emission failed! Order: 10 # <<-- My exception message

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

Последнее улучшение: упрощение

После более детального рассмотрения нашего окончательного кода, он кажется лучше, теперь его легко читать и поддерживать.

Но управляет ли OrderService бизнес-логикой? Я не думаю, что это сервис в общем смысле. Он больше похож на координацию вызовов настоящих сервисов обеспечивающих бизнес-логику, которая выглядит получше, чем паттерн facade.

Кроме того, можно заметить, что он запрашивает данные у status_service, чтобы что-то с ними сделать. (Что, на этот раз, действительно разрушает идею «Говори, а не спрашивай»).

Перейдем к упрощению.

class OrderFacade:  # Renamed to match what it actually is
    def emit(self, order_id: str) -> dict:
        try:
            # NOTE: info logging still happens inside
            status_service.ensure_order_unlocked(order_id)
            receipt_note = receipt_service.create(order_id)
            broker.emit_receipt_note(receipt_note)
            order_status = status_service.get_order_status(order_id)
        except OrderAlreadyInProgress as e:
            # New block
            logger.info("Aborting emission request because it is already in progress!")
            return {"order_id": order_id, "order_status": e.order_status.value}
        except OrderAlreadyEmitted as e:
            # New block
            logger.info(f"Aborting emission because it already happened! {e}")
            return {"order_id": order_id, "order_status": e.order_status.value}
        except OrderNotFound:
            logger.exception("We got a database exception")
            raise
        except ReceiptGenerationFailed:
            logger.exception("We got a problem generating the receipt")
            raise
        except ReceiptEmissionFailed:
            logger.exception("Unable to emit the receipt")
            raise
        else:
            return {"order_id": order_id, "order_status": order_status.value}

Мы только что создали новый метод ensure_order_unlocked для нашего status_service, который теперь отвечает за создание исключений/логирование в случае, если что-то идет не так.

Хорошо, а теперь скажите, насколько легче теперь стало это читать?

Я могу понять все return при беглом просмотре. Я знаю, что происходит, когда все идет хорошо, и как крайние случаи могут привести к разным результатам. И все это без прокрутки взад-вперед.

Теперь этот код такой же простой, каким (в основном) должен быть любой код.

Обратите внимание, что я решил вывести объект исключения e в логах, поскольку под капотом он будет запускать str(e), который вернет сообщение об исключении. Я подумал, что было бы полезно говорить подробно, поскольку мы не используем log.exception для этого блока, поэтому сообщение об исключении не будет отображаться.

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

Эффективное создание исключений

Всегда классифицируйте свои исключения через базовое и расширяйте все конкретные исключения от него. С помощью этой полезной практики вы можете переиспользовать логику для связанного кода.

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

# Base category exception
class OrderCreationException(Exception):
    pass

# Specific error with custom message. Order id is required.
class OrderNotFound(OrderCreationException):
    def __init__(self, order_id):
        self.order_id = order_id  # custom property
        super().__init__(
            f"Order {order_id} was not found in db "
            f"to emit."
        )


# Specific error with custom message. Order id is required.
class ReceiptGenerationFailed(OrderCreationException):
    def __init__(self, order_id):
        self.order_id = order_id  # custom property
        super().__init__(
            "Error found during emission! "
            f"Order: {order_id}"
        )

В примере выше я мог бы выйти за рамки и расширить базовый класс, чтобы всегда получать order_id, если мне это нужно. Этот совет поможет сохранить код сухим, поскольку мне не нужно быть многословным при создании исключений. Так можно использовать всего лишь одну переменную.

def func1(order_id):
    raise OrderNotFound(order_id)
    # instead of raise OrderNotFound(f"Can't find order {order_id}")


def func2(order_id):
    raise OrderNotFound(order_id)
    # instead of raise OrderNotFound(f"Can't find order {order_id}")

  В тестировании также будет больше смысла, поскольку я могу сделать assert order_id через строку.

assert e.order_id == order_id
# instead of assert order_id in str(e)

Ловим и создаем исключения эффективно

Еще одна вещь, которую люди часто делают неправильно – это отлавливают и повторно создают исключения.

Согласно PEP 3134 Python, делать нужно следующим образом.

Повторное создание исключения

Обычной инструкции raise более чем достаточно.

try:
    ...
except CustomException as ex:
    # do stuff (e.g. logging)
    raise

Создание одного исключения из другого

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

try:
    ...
except CustomException as ex:
    raise MyNewException() from ex

Эффективное логирование исключений

Еще один совет, который не позволит вам быть слишком многословным.

Используйте logger.exception

Вам не нужно логировать объект исключения. Функция exception логгера предназначена для использования внутри блоков except. Она уже обрабатывает трассировку стека с информацией о выполнении и отображает, какое исключение вызвало ее, с сообщением, установленном на уровне ошибки!

try:
    ...
except CustomException:
    logger.exception("custom message")

А что, если это не ошибка?

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

Вы можете принять решение установить exc_info в True, если хотите сохранить трассировку стека. Кроме того, было бы неплохо использовать объект исключения внутри сообщения.

Источники

Документация Python:

·  Python logging.logger.exception

·  Python PEP 3134

Принципы и качество кода:

·  Говори, а не спрашивай

·  Паттерн facade

·  Blob


РЕГИСТРАЦИЯ

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

Что такое исключения?

Разработка программы на любом языке довольно часто бывает связана с возникновением различного рода ошибок, препятствующих получению желаемого результата. Как правило, не все они приводят к моментальному завершению программы: некоторые ошибки можно ликвидировать еще на этапе компиляции, другие же способны оставаться незамеченными достаточно долгое время, а третьи и вовсе не зависят от программиста и возникают лишь в определенных ситуациях.

На сегодняшний день принято выделять такие типы ошибок:

  • Синтаксические – возникают из-за синтаксических погрешностей кода;
  • Логические – проявляются вследствие логических неточностей в алгоритме;
  • Исключения – вызваны некорректными действиями пользователя или системы.

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

print("Hello World!)

После попытки запуска выдастся текст ошибки:

  File "main.py", line 1
    print("Hello World!)
                       ^
SyntaxError: EOL while scanning string literal

Логические ошибки считаются более сложными в выявлении, поскольку не отлавливаются ни на этапе компиляции, ни во время выполнения готовой программы. Обычно они вызваны определенными недостатками в логике написанного алгоритма, из-за чего пользователь не получает желаемого результата. Ниже показана работа функции, которая должна вычислять среднее значение двух чисел, однако она этого не делает из-за неправильной формулы:

def avg(a, b):
    return a + b / 2
print(avg(10, 20))

20

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

print(10 / 0)

После попытки запуска будет выведено:

Traceback (most recent call last):
File "main.py", line 1, in <module>
print(10 / 0)
ZeroDivisionError: division by zero

Как можно заметить из результатов выполнения программы, деление на ноль провоцирует исключительную ситуацию, которая приводит к аварийному завершению работы и выводу ошибки на экран. В данном случае отображается файл, а также номер строки кода, где было выброшено исключение ZeroDivisionError. Ниже приводится краткое описание проблемы.

Перехват исключений

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

print("Program started")
print("Opening file...")
f = open("data.txt")
print("Program finished")

После запуск будет выведено:

Program started
Opening file...
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    f = open("data.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'

Так как файла с таким именем на жестком диске не обнаружено, программа сгенерировала исключение FileNotFoundError, сообщающее о проблеме с вводом/выводом. Последняя строка кода, в которой обозначается завершение программы, не была отображена на экране. Это значит, что не все операции, предусмотренные алгоритмом, были выполнены из-за проявившегося исключения.

Рассмотрим пример в Python конструкции try-except. Она позволяет обработать исключительную ситуацию без необходимости завершения уже работающей программы:

print("Program started")
try:
    print("Opening file...")
    f = open("data.txt")
except:
    print("File not found!")
print("Program finished")

Program started
Opening file...
File not found!
Program finished

Как видно из кода выше, блок try содержит опасный код, способный привести к ошибке (отсутствие нужного файла), а блок except включает в себя инструкции, которые следует выполнить в случае возникнувшей проблемы. Теперь если требуемый файл не был найден, программа не станет завершаться, о чем свидетельствует последний вывод функции print.

Несколько блоков except

Блоков except может быть несколько, в зависимости от того, какой тип исключения нужно обработать. Как правило, сначала обрабатываются более частные случаи, а затем общие:

print("Program started")
try:
    print("Opening file...")
    f = open("data.txt")
except FileNotFoundError:
    print("File not found!")
except Exception:
    print("Something gone wrong!")
print("Program finished")

Program started
Opening file...
File not found!
Program finished

Вложенные блоки и else

Блоки try-except могут быть вложенными для более гибкого управления исключениями. В следующем примере демонстрируется попытка открыть текстовый файл и записать в него некую строку. Для каждой цели используется отдельный блок try.

Также в данном примере используется конструкция else, которая выполняется в случае, если в коде не произошло исключений.

В данном случае — else сработает при успешной операции write. По умолчанию файл открывается на чтение в текстовом режиме. Поэтому при открытии файла будем использовать режим «w». В этом режиме файл открывается на запись. Если файла не было — создается новый, если был — перезаписывается.

print("Program started")
try:
    print("Opening file...")
    f = open("data.txt", "w")
    try:
        print("Writing to file...")
        f.write("Hello World!")
    except Exception:
        print("Something gone wrong!")
    else:
        print("Success!")
except FileNotFoundError:
    print("File not found!")
print("Program finished")

Program started
Opening file...
Writing to file...
Success!
Program finished

finally

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

print("Program started")
try:
    print("Opening file...")
    f = open("data.txt", "w")
    try:
        print("Writing to file...")
        f.write("Hello World!")
    except Exception:
        print("Something gone wrong!")
    else:
        print("Success!")
    finally:
        print("Closing file...")
        f.close()
except FileNotFoundError:
    print("File not found!")
print("Program finished")

Program started
Opening file...
Writing to file...
Success!
Closing file...
Program finished

Иногда такой подход к работе с текстовыми файлами может показаться слишком сложным, так как код, который его реализует, выглядит громоздким. Специально для этого существует конструкция with/as, позволяющая автоматизировать некоторые методы, такие как закрытие файла у соответствующего объекта. Таким образом, сокращается длина написанного кода:

print("Program started")
try:
    print("Writing to file...")
    with open("data.txt", "w") as f:
        f.write("Hello World!")
except Exception:
    print("Something gone wrong!")
print("Program finished")

Program started
Writing to file...
Program finished

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

Управление исключениями

Python позволяет создавать пользовательские исключения. Так же рассмотрим логирование программы.

Пользовательские исключения

Как правило, исключения автоматически вызываются в нужных ситуациях, однако в Python присутствует возможность запускать их самостоятельно. Для этого применяется ключевое слово raise. Следом за ним необходимо создать новый объект типа Exception, который потом можно обработать при помощи привычных конструкций try-except, как в данном примере:

print("Program started")
try:
    raise Exception("User Exception!")
except Exception as e:
    print(str(e))
print("Program finished")

Program started
User Exception!
Program finished

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

В следующем коде показан процесс генерации исключения NegativeAge, которое не позволяет пользователю ввести отрицательный возраст. Таким образом, экран выдает соответствующую ошибку:

class NegativeAge(Exception):
    pass
print("Program started")
try:
    age = int(input("Enter your age: "))
    if age < 0:
        raise NegativeAge("Exception: Negative age!")
    print("Success!")
except NegativeAge as e:
    print(e)
print("Program finished")

Program started
Enter your age: -18
Exception: Negative age!
Program finished

Запись в лог

Для вывода специальных сообщений, не влияющих на функционирование программы, в Python применяется библиотека логов. Чтобы воспользоваться ею, необходимо выполнить импорт в верхней части файла. Существует несколько типов логов, отобразить которые поможет команда, определенная во второй строке кода в следующем примере программы:

import logging
logging.basicConfig(level = logging.DEBUG)
logging.debug("Debug message!")
logging.info("Info message!")
logging.warning("Warning message!")
logging.error("Error message!")
logging.critical("Critical message!")

DEBUG:root:Debug message!
INFO:root:Info message!
WARNING:root:Warning message!
ERROR:root:Error message!
CRITICAL:root:Critical message!

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

С помощью logging на Python можно записывать в лог и исключения. Обычно лог пишется в файл, зададим его как log.txt. Уровень INFO указывает, что сообщения уровней ниже (в данном случае debug) не будут отражаться в логе. Python позволяет в try-except получить текст ошибки, который и запишем:

import logging
logging.basicConfig(filename="log.txt", level = logging.INFO)
try:
    print(10 / 0)
except Exception as e:
    logging.error(str(e))

В log .txt будет добавлена строка сообщения о типе сработавшего исключения «ERROR:root:division by zero».

Иерархия исключений

В языке программирования Python присутствует строгая иерархия исключений. Вершиной является BaseException, включающий в себя все существующие разновидности исключений:

  • SystemExit – возникает при выходе из программы с помощью sys.exit;
  • KeyboardInterrupt – указывает на прерывание программы пользователем;
  • GeneratorExit – появляется при вызове метода close для объекта generator;
  • Exception – представляет совокупность обычных несистемных исключений.

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

Название Характеристика
ArithmeticError Порождается арифметическими ошибками (операции с плавающей точкой, переполнение числовой переменной, деление на ноль)
AssertionError Возникает при ложном выражении в функции assert
AttributeError Появляется в случаях, когда нужный атрибут объекта отсутствует
BufferError Указывает на невозможность выполнения буферной операции
EOFError Проявляется, когда функция не смогла прочитать конец файла
ImportError Сообщает о неудачном импорте модуля либо атрибута
LookupError Информирует про недействительный индекс или ключ в массиве
MemoryError Возникает в ситуации, когда доступной памяти недостаточно
NameError Указывает на ошибку при поиске переменной с нужным именем
NotImplementedError Предупреждает о том, что абстрактные методы класса должны быть обязательно переопределены в классах-наследниках
OSError Включает в себя системные ошибки (отсутствие доступа к нужному файлу или директории, проблемы с поиском процессов)
ReferenceError Порождается попыткой доступа к атрибуту со слабой ссылкой
RuntimeError Сообщает об исключении, которое не классифицируется
StopIteration Возникает во время работы функции next при отсутствии элементов
SyntaxError Представляет собой совокупность синтаксических ошибок
SystemError Порождается внутренними ошибками системы
TypeError Указывает на то, что операция не может быть выполнена с объектом
UnicodeError Сообщает о неправильной кодировке символов в программе
ValueError Возникает при получении некорректного значения для переменной
Warning Обозначает предупреждение

Заключение

Исключения являются самостоятельным типом данных, при помощи которого программа сообщает пользователю о различных ошибках. Обработка исключений позволяет менять сценарий выполнения программы с учетом обстоятельств, так или иначе блокирующих ее нормальную работу. Для удобного управления исключениями в языке программирования Python предусмотрены специальные синтаксические конструкции. Кроме того, в нем есть большая библиотека готовых исключений, а также возможность создавать собственные.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Pyscripter could not load a python engine ошибка
  • Pyqt5 диалоговое окно ошибки