While writing a python function and using it in your program, you get lot of errors such typing wrong name of the function, changing the order of definition and function calls, wrong parameters, less parameters(parameter mismatch) and scope errors.
Wrong Function Definition and Function Call Order
In python programming one of the most common error is calling the function even before you define your function. The function definition runs first and creates the function in python programming before you can call it. If you call the function earlier than python will throw an error. Consider the following example.
print(add(23,67))
def add(n1, n2):
return n1 + n2
The program above will try to execute print statement – print(23, 67)
and it will not find the function add().
This will result in following error.
= RESTART: C:/PythonExercises/Wrong_definition_function_call_order.py
Traceback (most recent call last):
File "C:/PROGRAMMING/PythonExercises/Wrong_definition_function_call_order.py", line 1, in <module>
print(add(23,67))
NameError: name 'add' is not defined
>>>
Parameter Mismatch Error
A lot of python functions are written with multiple parameters.However, when you call the function with either wrong parameter type or less number of parameters, python will throw an error in each case.
def expression_1(num1, num2, num3):
return (num1 + num2)/num3
# But the function call has wrong type
expression_1("345", 55, 66)
Consider the parameter type mismatch error example. In the program below, the function is expecting a number, but receives a string number. Therefore, throws an error.
=== RESTART: C:/PythonExercises/Functions/type_mismatch_error.py ===
Traceback (most recent call last):
File "C:/PythonExercises/Functions/type_mismatch_error.py", line 5, in <module>
expression_1("345", 55, 66)
File "C:/PythonExercises/Functions/type_mismatch_error.py", line 2, in expression_1
return (num1 + num2)/num3
TypeError: can only concatenate str (not "int") to str
From the above example, it clearly evident that there is a type mismatch between the function call and function definition.
The second type of error happens when the function call does not have the correct number of arguments, according to parameters.
# function definition
def expression_2(num1, num2, num3):
return num1 * num2 + num3
# function call with only two parameters, missing third
expression_2(10,44)
The above program missing the third argument according to third parameter in the function definition. Note that the argument is the value replaced with the parameter. Therefore, number of arguments must match number of parameters.
Traceback (most recent call last):
File "C:/PROGRAMMING/PythonExercises/Functions/number_of_arguments_mismatch_error.py", line 5, in <module>
expression_2(10,44)
TypeError: expression_2() missing 1 required positional argument: 'num3'
>>>
The error clearly says that the num3
is missing.
Scope Error
In python, a local variable that is created inside of the function cannot be called outside of the function. This is possible in the case of a conditional block such as if, if-then-else, etc. But not in python functions.This is called a scope error. Consider the following example. This example tries to find the maximum of two numbers.
def find_max(n1, n2):
num1 = n1
num2 = n2
if num1 > num2:
result = num1
elif num2 > num1:
result = num2
else:
result = num1
find_max(34,77)
print(result)
The program tries to print the variable called result created inside of the function. But it does not work and gives following error.
Traceback (most recent call last):
File "C:/PROGRAMMING/PythonExercises/Functions/Scope_Error.py", line 13, in <module>
print(result)
NameError: name 'result' is not defined
To fix the problem, you have to assign the function itself to a variable and then print it. Consider the modified program below.
def find_max(n1, n2):
num1 = n1
num2 = n2
if num1 > num2:
result = num1
elif num2 > num1:
result = num2
else:
result = num1
return result
maxNum = find_max(34,77)
print(maxNum)
The output of the program is printed successfully.
===== RESTART: C:/PythonExercises/Scope_Error.py =======
77
>>>
In the next article, we will talk about function that returns None type.
Обработка ошибок увеличивает отказоустойчивость кода, защищая его от потенциальных сбоев, которые могут привести к преждевременному завершению работы.
Прежде чем переходить к обсуждению того, почему обработка исключений так важна, и рассматривать встроенные в Python исключения, важно понять, что есть тонкая грань между понятиями ошибки и исключения.
Ошибку нельзя обработать, а исключения Python обрабатываются при выполнении программы. Ошибка может быть синтаксической, но существует и много видов исключений, которые возникают при выполнении и не останавливают программу сразу же. Ошибка может указывать на критические проблемы, которые приложение и не должно перехватывать, а исключения — состояния, которые стоит попробовать перехватить. Ошибки — вид непроверяемых и невозвратимых ошибок, таких как OutOfMemoryError
, которые не стоит пытаться обработать.
Обработка исключений делает код более отказоустойчивым и помогает предотвращать потенциальные проблемы, которые могут привести к преждевременной остановке выполнения. Представьте код, который готов к развертыванию, но все равно прекращает работу из-за исключения. Клиент такой не примет, поэтому стоит заранее обработать конкретные исключения, чтобы избежать неразберихи.
Ошибки могут быть разных видов:
- Синтаксические
- Недостаточно памяти
- Ошибки рекурсии
- Исключения
Разберем их по очереди.
Синтаксические ошибки (SyntaxError)
Синтаксические ошибки часто называют ошибками разбора. Они возникают, когда интерпретатор обнаруживает синтаксическую проблему в коде.
Рассмотрим на примере.
a = 8
b = 10
c = a b
File "", line 3
c = a b
^
SyntaxError: invalid syntax
Стрелка вверху указывает на место, где интерпретатор получил ошибку при попытке исполнения. Знак перед стрелкой указывает на причину проблемы. Для устранения таких фундаментальных ошибок Python будет делать большую часть работы за программиста, выводя название файла и номер строки, где была обнаружена ошибка.
Недостаточно памяти (OutofMemoryError)
Ошибки памяти чаще всего связаны с оперативной памятью компьютера и относятся к структуре данных под названием “Куча” (heap
). Если есть крупные объекты (или) ссылки на подобные, то с большой долей вероятности возникнет ошибка OutofMemory
. Она может появиться по нескольким причинам:
- Использование 32-битной архитектуры Python (максимальный объем выделенной памяти невысокий, между 2 и 4 ГБ);
- Загрузка файла большого размера;
- Запуск модели машинного обучения/глубокого обучения и много другое;
Обработать ошибку памяти можно с помощью обработки исключений — резервного исключения. Оно используется, когда у интерпретатора заканчивается память и он должен немедленно остановить текущее исполнение. В редких случаях Python вызывает OutofMemoryError
, позволяя скрипту каким-то образом перехватить самого себя, остановить ошибку памяти и восстановиться.
Но поскольку Python использует архитектуру управления памятью из языка C (функция malloc()
), не факт, что все процессы восстановятся — в некоторых случаях MemoryError
приведет к остановке. Следовательно, обрабатывать такие ошибки не рекомендуется, и это не считается хорошей практикой.
Ошибка рекурсии (RecursionError)
Эта ошибка связана со стеком и происходит при вызове функций. Как и предполагает название, ошибка рекурсии возникает, когда внутри друг друга исполняется много методов (один из которых — с бесконечной рекурсией), но это ограничено размером стека.
Все локальные переменные и методы размещаются в стеке. Для каждого вызова метода создается стековый кадр (фрейм), внутрь которого помещаются данные переменной или результат вызова метода. Когда исполнение метода завершается, его элемент удаляется.
Чтобы воспроизвести эту ошибку, определим функцию recursion
, которая будет рекурсивной — вызывать сама себя в бесконечном цикле. В результате появится ошибка StackOverflow
или ошибка рекурсии, потому что стековый кадр будет заполняться данными метода из каждого вызова, но они не будут освобождаться.
def recursion():
return recursion()
recursion()
---------------------------------------------------------------------------
RecursionError Traceback (most recent call last)
in
----> 1 recursion()
in recursion()
1 def recursion():
----> 2 return recursion()
... last 1 frames repeated, from the frame below ...
in recursion()
1 def recursion():
----> 2 return recursion()
RecursionError: maximum recursion depth exceeded
Ошибка отступа (IndentationError)
Эта ошибка похожа по духу на синтаксическую и является ее подвидом. Тем не менее она возникает только в случае проблем с отступами.
Пример:
for i in range(10):
print('Привет Мир!')
File "", line 2
print('Привет Мир!')
^
IndentationError: expected an indented block
Исключения
Даже если синтаксис в инструкции или само выражение верны, они все равно могут вызывать ошибки при исполнении. Исключения Python — это ошибки, обнаруживаемые при исполнении, но не являющиеся критическими. Скоро вы узнаете, как справляться с ними в программах Python. Объект исключения создается при вызове исключения Python. Если скрипт не обрабатывает исключение явно, программа будет остановлена принудительно.
Программы обычно не обрабатывают исключения, что приводит к подобным сообщениям об ошибке:
Ошибка типа (TypeError)
a = 2
b = 'PythonRu'
a + b
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in
1 a = 2
2 b = 'PythonRu'
----> 3 a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Ошибка деления на ноль (ZeroDivisionError)
10 / 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
in
----> 1 10 / 0
ZeroDivisionError: division by zero
Есть разные типы исключений в Python и их тип выводится в сообщении: вверху примеры TypeError
и ZeroDivisionError
. Обе строки в сообщениях об ошибке представляют собой имена встроенных исключений 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
Прежде чем переходить к разбору встроенных исключений быстро вспомним 4 основных компонента обработки исключения, как показано на этой схеме.
Try
: он запускает блок кода, в котором ожидается ошибка.Except
: здесь определяется тип исключения, который ожидается в блокеtry
(встроенный или созданный).Else
: если исключений нет, тогда исполняется этот блок (его можно воспринимать как средство для запуска кода в том случае, если ожидается, что часть кода приведет к исключению).Finally
: вне зависимости от того, будет ли исключение или нет, этот блок кода исполняется всегда.
В следующем разделе руководства больше узнаете об общих типах исключений и научитесь обрабатывать их с помощью инструмента обработки исключения.
Ошибка прерывания с клавиатуры (KeyboardInterrupt)
Исключение KeyboardInterrupt
вызывается при попытке остановить программу с помощью сочетания Ctrl + C
или Ctrl + Z
в командной строке или ядре в Jupyter Notebook. Иногда это происходит неумышленно и подобная обработка поможет избежать подобных ситуаций.
В примере ниже если запустить ячейку и прервать ядро, программа вызовет исключение KeyboardInterrupt
. Теперь обработаем исключение KeyboardInterrupt
.
try:
inp = input()
print('Нажмите Ctrl+C и прервите Kernel:')
except KeyboardInterrupt:
print('Исключение KeyboardInterrupt')
else:
print('Исключений не произошло')
Исключение KeyboardInterrupt
Стандартные ошибки (StandardError)
Рассмотрим некоторые базовые ошибки в программировании.
Арифметические ошибки (ArithmeticError)
- Ошибка деления на ноль (Zero Division);
- Ошибка переполнения (OverFlow);
- Ошибка плавающей точки (Floating Point);
Все перечисленные выше исключения относятся к классу Arithmetic
и вызываются при ошибках в арифметических операциях.
Деление на ноль (ZeroDivisionError)
Когда делитель (второй аргумент операции деления) или знаменатель равны нулю, тогда результатом будет ошибка деления на ноль.
try:
a = 100 / 0
print(a)
except ZeroDivisionError:
print("Исключение ZeroDivisionError." )
else:
print("Успех, нет ошибок!")
Исключение ZeroDivisionError.
Переполнение (OverflowError)
Ошибка переполнение вызывается, когда результат операции выходил за пределы диапазона. Она характерна для целых чисел вне диапазона.
try:
import math
print(math.exp(1000))
except OverflowError:
print("Исключение OverFlow.")
else:
print("Успех, нет ошибок!")
Исключение OverFlow.
Ошибка утверждения (AssertionError)
Когда инструкция утверждения не верна, вызывается ошибка утверждения.
Рассмотрим пример. Предположим, есть две переменные: a
и b
. Их нужно сравнить. Чтобы проверить, равны ли они, необходимо использовать ключевое слово assert
, что приведет к вызову исключения Assertion
в том случае, если выражение будет ложным.
try:
a = 100
b = "PythonRu"
assert a == b
except AssertionError:
print("Исключение AssertionError.")
else:
print("Успех, нет ошибок!")
Исключение AssertionError.
Ошибка атрибута (AttributeError)
При попытке сослаться на несуществующий атрибут программа вернет ошибку атрибута. В следующем примере можно увидеть, что у объекта класса Attributes
нет атрибута с именем attribute
.
class Attributes(obj):
a = 2
print(a)
try:
obj = Attributes()
print(obj.attribute)
except AttributeError:
print("Исключение AttributeError.")
2
Исключение AttributeError.
Ошибка импорта (ModuleNotFoundError)
Ошибка импорта вызывается при попытке импортировать несуществующий (или неспособный загрузиться) модуль в стандартном пути или даже при допущенной ошибке в имени.
import nibabel
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
in
----> 1 import nibabel
ModuleNotFoundError: No module named 'nibabel'
Ошибка поиска (LookupError)
LockupError
выступает базовым классом для исключений, которые происходят, когда key
или index
используются для связывания или последовательность списка/словаря неверна или не существует.
Здесь есть два вида исключений:
- Ошибка индекса (
IndexError
); - Ошибка ключа (
KeyError
);
Ошибка ключа
Если ключа, к которому нужно получить доступ, не оказывается в словаре, вызывается исключение KeyError
.
try:
a = {1:'a', 2:'b', 3:'c'}
print(a[4])
except LookupError:
print("Исключение KeyError.")
else:
print("Успех, нет ошибок!")
Исключение KeyError.
Ошибка индекса
Если пытаться получить доступ к индексу (последовательности) списка, которого не существует в этом списке или находится вне его диапазона, будет вызвана ошибка индекса (IndexError: list index out of range python).
try:
a = ['a', 'b', 'c']
print(a[4])
except LookupError:
print("Исключение IndexError, индекс списка вне диапазона.")
else:
print("Успех, нет ошибок!")
Исключение IndexError, индекс списка вне диапазона.
Ошибка памяти (MemoryError)
Как уже упоминалось, ошибка памяти вызывается, когда операции не хватает памяти для выполнения.
Ошибка имени (NameError)
Ошибка имени возникает, когда локальное или глобальное имя не находится.
В следующем примере переменная ans
не определена. Результатом будет ошибка NameError
.
try:
print(ans)
except NameError:
print("NameError: переменная 'ans' не определена")
else:
print("Успех, нет ошибок!")
NameError: переменная 'ans' не определена
Ошибка выполнения (Runtime Error)
Ошибка «NotImplementedError»
Ошибка выполнения служит базовым классом для ошибки NotImplemented
. Абстрактные методы определенного пользователем класса вызывают это исключение, когда производные методы перезаписывают оригинальный.
class BaseClass(object):
"""Опередляем класс"""
def __init__(self):
super(BaseClass, self).__init__()
def do_something(self):
# функция ничего не делает
raise NotImplementedError(self.__class__.__name__ + '.do_something')
class SubClass(BaseClass):
"""Реализует функцию"""
def do_something(self):
# действительно что-то делает
print(self.__class__.__name__ + ' что-то делает!')
SubClass().do_something()
BaseClass().do_something()
SubClass что-то делает!
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
in
14
15 SubClass().do_something()
---> 16 BaseClass().do_something()
in do_something(self)
5 def do_something(self):
6 # функция ничего не делает
----> 7 raise NotImplementedError(self.__class__.__name__ + '.do_something')
8
9 class SubClass(BaseClass):
NotImplementedError: BaseClass.do_something
Ошибка типа (TypeError)
Ошибка типа вызывается при попытке объединить два несовместимых операнда или объекта.
В примере ниже целое число пытаются добавить к строке, что приводит к ошибке типа.
try:
a = 5
b = "PythonRu"
c = a + b
except TypeError:
print('Исключение TypeError')
else:
print('Успех, нет ошибок!')
Исключение TypeError
Ошибка значения (ValueError)
Ошибка значения вызывается, когда встроенная операция или функция получают аргумент с корректным типом, но недопустимым значением.
В этом примере встроенная операция float
получат аргумент, представляющий собой последовательность символов (значение), что является недопустимым значением для типа: число с плавающей точкой.
try:
print(float('PythonRu'))
except ValueError:
print('ValueError: не удалось преобразовать строку в float: 'PythonRu'')
else:
print('Успех, нет ошибок!')
ValueError: не удалось преобразовать строку в float: 'PythonRu'
Пользовательские исключения в Python
В Python есть много встроенных исключений для использования в программе. Но иногда нужно создавать собственные со своими сообщениями для конкретных целей.
Это можно сделать, создав новый класс, который будет наследовать из класса Exception
в Python.
class UnAcceptedValueError(Exception):
def __init__(self, data):
self.data = data
def __str__(self):
return repr(self.data)
Total_Marks = int(input("Введите общее количество баллов: "))
try:
Num_of_Sections = int(input("Введите количество разделов: "))
if(Num_of_Sections < 1):
raise UnAcceptedValueError("Количество секций не может быть меньше 1")
except UnAcceptedValueError as e:
print("Полученная ошибка:", e.data)
Введите общее количество баллов: 10
Введите количество разделов: 0
Полученная ошибка: Количество секций не может быть меньше 1
В предыдущем примере если ввести что-либо меньше 1, будет вызвано исключение. Многие стандартные исключения имеют собственные исключения, которые вызываются при возникновении проблем в работе их функций.
Недостатки обработки исключений в Python
У использования исключений есть свои побочные эффекты, как, например, то, что программы с блоками try-except работают медленнее, а количество кода возрастает.
Дальше пример, где модуль Python timeit
используется для проверки времени исполнения 2 разных инструкций. В stmt1
для обработки ZeroDivisionError
используется try-except, а в stmt2
— if
. Затем они выполняются 10000 раз с переменной a=0
. Суть в том, чтобы показать разницу во времени исполнения инструкций. Так, stmt1
с обработкой исключений занимает больше времени чем stmt2
, который просто проверяет значение и не делает ничего, если условие не выполнено.
Поэтому стоит ограничить использование обработки исключений в Python и применять его в редких случаях. Например, когда вы не уверены, что будет вводом: целое или число с плавающей точкой, или не уверены, существует ли файл, который нужно открыть.
import timeit
setup="a=0"
stmt1 = '''
try:
b=10/a
except ZeroDivisionError:
pass'''
stmt2 = '''
if a!=0:
b=10/a'''
print("time=",timeit.timeit(stmt1,setup,number=10000))
print("time=",timeit.timeit(stmt2,setup,number=10000))
time= 0.003897680000136461
time= 0.0002797570000439009
Выводы!
Как вы могли увидеть, обработка исключений помогает прервать типичный поток программы с помощью специального механизма, который делает код более отказоустойчивым.
Обработка исключений — один из основных факторов, который делает код готовым к развертыванию. Это простая концепция, построенная всего на 4 блоках: try
выискивает исключения, а except
их обрабатывает.
Очень важно поупражняться в их использовании, чтобы сделать свой код более отказоустойчивым.
Errors¶
Errors or mistakes in a program are often referred to as bugs. They are almost always the fault of the programmer. The process of finding and eliminating errors is called debugging. Errors can be classified into three major groups:
- Syntax errors
- Runtime errors
- Logical errors
Syntax errors¶
Python will find these kinds of errors when it tries to parse your program, and exit with an error message without running anything. Syntax errors are mistakes in the use of the Python language, and are analogous to spelling or grammar mistakes in a language like English: for example, the sentence Would you some tea? does not make sense – it is missing a verb.
Common Python syntax errors include:
- leaving out a keyword
- putting a keyword in the wrong place
- leaving out a symbol, such as a colon, comma or brackets
- misspelling a keyword
- incorrect indentation
- empty block
Note
it is illegal for any block (like an if
body, or the body of a function) to be left completely empty. If you want a block to do nothing, you can use the pass
statement inside the block.
Python will do its best to tell you where the error is located, but sometimes its messages can be misleading: for example, if you forget to escape a quotation mark inside a string you may get a syntax error referring to a place later in your code, even though that is not the real source of the problem. If you can’t see anything wrong on the line specified in the error message, try backtracking through the previous few lines. As you program more, you will get better at identifying and fixing errors.
Here are some examples of syntax errors in Python:
myfunction(x, y): return x + y else: print("Hello!") if mark >= 50 print("You passed!") if arriving: print("Hi!") esle: print("Bye!") if flag: print("Flag is set!")
Runtime errors¶
If a program is syntactically correct – that is, free of syntax errors – it will be run by the Python interpreter. However, the program may exit unexpectedly during execution if it encounters a runtime error – a problem which was not detected when the program was parsed, but is only revealed when a particular line is executed. When a program comes to a halt because of a runtime error, we say that it has crashed.
Consider the English instruction flap your arms and fly to Australia. While the instruction is structurally correct and you can understand its meaning perfectly, it is impossible for you to follow it.
Some examples of Python runtime errors:
- division by zero
- performing an operation on incompatible types
- using an identifier which has not been defined
- accessing a list element, dictionary value or object attribute which doesn’t exist
- trying to access a file which doesn’t exist
Runtime errors often creep in if you don’t consider all possible values that a variable could contain, especially when you are processing user input. You should always try to add checks to your code to make sure that it can deal with bad input and edge cases gracefully. We will look at this in more detail in the chapter about exception handling.
Logical errors¶
Logical errors are the most difficult to fix. They occur when the program runs without crashing, but produces an incorrect result. The error is caused by a mistake in the program’s logic. You won’t get an error message, because no syntax or runtime error has occurred. You will have to find the problem on your own by reviewing all the relevant parts of your code – although some tools can flag suspicious code which looks like it could cause unexpected behaviour.
Sometimes there can be absolutely nothing wrong with your Python implementation of an algorithm – the algorithm itself can be incorrect. However, more frequently these kinds of errors are caused by programmer carelessness. Here are some examples of mistakes which lead to logical errors:
- using the wrong variable name
- indenting a block to the wrong level
- using integer division instead of floating-point division
- getting operator precedence wrong
- making a mistake in a boolean expression
- off-by-one, and other numerical errors
If you misspell an identifier name, you may get a runtime error or a logical error, depending on whether the misspelled name is defined.
A common source of variable name mix-ups and incorrect indentation is frequent copying and pasting of large blocks of code. If you have many duplicate lines with minor differences, it’s very easy to miss a necessary change when you are editing your pasted lines. You should always try to factor out excessive duplication using functions and loops – we will look at this in more detail later.
Exercise 1¶
-
Find all the syntax errors in the code snippet above, and explain why they are errors.
-
Find potential sources of runtime errors in this code snippet:
dividend = float(input("Please enter the dividend: ")) divisor = float(input("Please enter the divisor: ")) quotient = dividend / divisor quotient_rounded = math.round(quotient)
-
Find potential sources of runtime errors in this code snippet:
for x in range(a, b): print("(%f, %f, %f)" % my_list[x])
-
Find potential sources of logic errors in this code snippet:
product = 0 for i in range(10): product *= i sum_squares = 0 for i in range(10): i_sq = i**2 sum_squares += i_sq nums = 0 for num in range(10): num += num
Handling exceptions¶
Until now, the programs that we have written have generally ignored the fact that things can go wrong. We have have tried to prevent runtime errors by checking data which may be incorrect before we used it, but we haven’t yet seen how we can handle errors when they do occur – our programs so far have just crashed suddenly whenever they have encountered one.
There are some situations in which runtime errors are likely to occur. Whenever we try to read a file or get input from a user, there is a chance that something unexpected will happen – the file may have been moved or deleted, and the user may enter data which is not in the right format. Good programmers should add safeguards to their programs so that common situations like this can be handled gracefully – a program which crashes whenever it encounters an easily foreseeable problem is not very pleasant to use. Most users expect programs to be robust enough to recover from these kinds of setbacks.
If we know that a particular section of our program is likely to cause an error, we can tell Python what to do if it does happen. Instead of letting the error crash our program we can intercept it, do something about it, and allow the program to continue.
All the runtime (and syntax) errors that we have encountered are called exceptions in Python – Python uses them to indicate that something exceptional has occurred, and that your program cannot continue unless it is handled. All exceptions are subclasses of the Exception
class – we will learn more about classes, and how to write your own exception types, in later chapters.
The try
and except
statements¶
To handle possible exceptions, we use a try-except block:
try: age = int(input("Please enter your age: ")) print("I see that you are %d years old." % age) except ValueError: print("Hey, that wasn't a number!")
Python will try to process all the statements inside the try block. If a ValueError
occurs at any point as it is executing them, the flow of control will immediately pass to the except block, and any remaining statements in the try block will be skipped.
In this example, we know that the error is likely to occur when we try to convert the user’s input to an integer. If the input string is not a number, this line will trigger a ValueError
– that is why we specified it as the type of error that we are going to handle.
We could have specified a more general type of error – or even left the type out entirely, which would have caused the except
clause to match any kind of exception – but that would have been a bad idea. What if we got a completely different error that we hadn’t predicted? It would be handled as well, and we wouldn’t even notice that anything unusual was going wrong. We may also want to react in different ways to different kinds of errors. We should always try pick specific rather than general error types for our except
clauses.
It is possible for one except
clause to handle more than one kind of error: we can provide a tuple of exception types instead of a single type:
try: dividend = int(input("Please enter the dividend: ")) divisor = int(input("Please enter the divisor: ")) print("%d / %d = %f" % (dividend, divisor, dividend/divisor)) except(ValueError, ZeroDivisionError): print("Oops, something went wrong!")
A try-except block can also have multiple except
clauses. If an exception occurs, Python will check each except
clause from the top down to see if the exception type matches. If none of the except
clauses match, the exception will be considered unhandled, and your program will crash:
try: dividend = int(input("Please enter the dividend: ")) divisor = int(input("Please enter the divisor: ")) print("%d / %d = %f" % (dividend, divisor, dividend/divisor)) except ValueError: print("The divisor and dividend have to be numbers!") except ZeroDivisionError: print("The dividend may not be zero!")
Note that in the example above if a ValueError
occurs we won’t know whether it was caused by the dividend or the divisor not being an integer – either one of the input lines could cause that error. If we want to give the user more specific feedback about which input was wrong, we will have to wrap each input line in a separate try-except block:
try: dividend = int(input("Please enter the dividend: ")) except ValueError: print("The dividend has to be a number!") try: divisor = int(input("Please enter the divisor: ")) except ValueError: print("The divisor has to be a number!") try: print("%d / %d = %f" % (dividend, divisor, dividend/divisor)) except ZeroDivisionError: print("The dividend may not be zero!")
In general, it is a better idea to use exception handlers to protect small blocks of code against specific errors than to wrap large blocks of code and write vague, generic error recovery code. It may sometimes seem inefficient and verbose to write many small try-except statements instead of a single catch-all statement, but we can mitigate this to some extent by making effective use of loops and functions to reduce the amount of code duplication.
How an exception is handled¶
When an exception occurs, the normal flow of execution is interrupted. Python checks to see if the line of code which caused the exception is inside a try block. If it is, it checks to see if any of the except blocks associated with the try block can handle that type of exception. If an appropriate handler is found, the exception is handled, and the program continues from the next statement after the end of that try-except.
If there is no such handler, or if the line of code was not in a try block, Python will go up one level of scope: if the line of code which caused the exception was inside a function, that function will exit immediately, and the line which called the function will be treated as if it had thrown the exception. Python will check if that line is inside a try block, and so on. When a function is called, it is placed on Python’s stack, which we will discuss in the chapter about functions. Python traverses this stack when it tries to handle an exception.
If an exception is thrown by a line which is in the main body of your program, not inside a function, the program will terminate. When the exception message is printed, you should also see a traceback – a list which shows the path the exception has taken, all the way back to the original line which caused the error.
Error checks vs exception handling¶
Exception handling gives us an alternative way to deal with error-prone situations in our code. Instead of performing more checks before we do something to make sure that an error will not occur, we just try to do it – and if an error does occur we handle it. This can allow us to write simpler and more readable code. Let’s look at a more complicated input example – one in which we want to keep asking the user for input until the input is correct. We will try to write this example using the two different approaches:
# with checks n = None while n is None: s = input("Please enter an integer: ") if s.lstrip('-').isdigit(): n = int(s) else: print("%s is not an integer." % s) # with exception handling n = None while n is None: try: s = input("Please enter an integer: ") n = int(s) except ValueError: print("%s is not an integer." % s)
In the first code snippet, we have to write quite a convoluted check to test whether the user’s input is an integer – first we strip off a minus sign if it exists, and then we check if the rest of the string consists only of digits. But there’s a very simple criterion which is also what we really want to know: will this string cause a ValueError
if we try to convert it to an integer? In the second snippet we can in effect check for exactly the right condition instead of trying to replicate it ourselves – something which isn’t always easy to do. For example, we could easily have forgotten that integers can be negative, and written the check in the first snippet incorrectly.
Here are a few other advantages of exception handling:
- It separates normal code from code that handles errors.
- Exceptions can easily be passed along functions in the stack until they reach a function which knows how to handle them. The intermediate functions don’t need to have any error-handling code.
- Exceptions come with lots of useful error information built in – for example, they can print a traceback which helps us to see exactly where the error occurred.
The else
and finally
statements¶
There are two other clauses that we can add to a try-except block: else
and finally
. else
will be executed only if the try
clause doesn’t raise an exception:
try: age = int(input("Please enter your age: ")) except ValueError: print("Hey, that wasn't a number!") else: print("I see that you are %d years old." % age)
We want to print a message about the user’s age only if the integer conversion succeeds. In the first exception handler example, we put this print statement directly after the conversion inside the try
block. In both cases, the statement will only be executed if the conversion statement doesn’t raise an exception, but putting it in the else
block is better practice – it means that the only code inside the try
block is the single line that is the potential source of the error that we want to handle.
When we edit this program in the future, we may introduce additional statements that should also be executed if the age input is successfully converted. Some of these statements may also potentially raise a ValueError
. If we don’t notice this, and put them inside the try
clause, the except
clause will also handle these errors if they occur. This is likely to cause some odd and unexpected behaviour. By putting all this extra code in the else
clause instead, we avoid taking this risk.
The finally
clause will be executed at the end of the try-except block no matter what – if there is no exception, if an exception is raised and handled, if an exception is raised and not handled, and even if we exit the block using break
, continue
or return
. We can use the finally
clause for cleanup code that we always want to be executed:
try: age = int(input("Please enter your age: ")) except ValueError: print("Hey, that wasn't a number!") else: print("I see that you are %d years old." % age) finally: print("It was really nice talking to you. Goodbye!")
Exercise 2¶
-
Extend the program in exercise 7 of the loop control statements chapter to include exception handling. Whenever the user enters input of the incorrect type, keep prompting the user for the same value until it is entered correctly. Give the user sensible feedback.
-
Add a try-except statement to the body of this function which handles a possible
IndexError
, which could occur if the index provided exceeds the length of the list. Print an error message if this happens:def print_list_element(thelist, index): print(thelist[index])
-
This function adds an element to a list inside a dict of lists. Rewrite it to use a try-except statement which handles a possible
KeyError
if the list with the name provided doesn’t exist in the dictionary yet, instead of checking beforehand whether it does. Includeelse
andfinally
clauses in your try-except block:def add_to_list_in_dict(thedict, listname, element): if listname in thedict: l = thedict[listname] print("%s already has %d elements." % (listname, len(l))) else: thedict[listname] = [] print("Created %s." % listname) thedict[listname].append(element) print("Added %s to %s." % (element, listname))
The with
statement¶
Using the exception object¶
Python’s exception objects contain more information than just the error type. They also come with some kind of message – we have already seen some of these messages displayed when our programs have crashed. Often these messages aren’t very user-friendly – if we want to report an error to the user we usually need to write a more descriptive message which explains how the error is related to what the user did. For example, if the error was caused by incorrect input, it is helpful to tell the user which of the input values was incorrect.
Sometimes the exception message contains useful information which we want to display to the user. In order to access the message, we need to be able to access the exception object. We can assign the object to a variable that we can use inside the except
clause like this:
try: age = int(input("Please enter your age: ")) except ValueError as err: print(err)
err
is not a string, but Python knows how to convert it into one – the string representation of an exception is the message, which is exactly what we want. We can also combine the exception message with our own message:
try: age = int(input("Please enter your age: ")) except ValueError as err: print("You entered incorrect age input: %s" % err)
Note that inserting a variable into a formatted string using %s
also converts the variable to a string.
Raising exceptions¶
We can raise exceptions ourselves using the raise
statement:
try: age = int(input("Please enter your age: ")) if age < 0: raise ValueError("%d is not a valid age. Age must be positive or zero.") except ValueError as err: print("You entered incorrect age input: %s" % err) else: print("I see that you are %d years old." % age)
We can raise our own ValueError
if the age input is a valid integer, but it’s negative. When we do this, it has exactly the same effect as any other exception – the flow of control will immediately exit the try
clause at this point and pass to the except
clause. This except
clause can match our exception as well, since it is also a ValueError
.
We picked ValueError
as our exception type because it’s the most appropriate for this kind of error. There’s nothing stopping us from using a completely inappropriate exception class here, but we should try to be consistent. Here are a few common exception types which we are likely to raise in our own code:
TypeError
: this is an error which indicates that a variable has the wrong type for some operation. We might raise it in a function if a parameter is not of a type that we know how to handle.ValueError
: this error is used to indicate that a variable has the right type but the wrong value. For example, we used it whenage
was an integer, but the wrong kind of integer.NotImplementedError
: we will see in the next chapter how we use this exception to indicate that a class’s method has to be implemented in a child class.
We can also write our own custom exception classes which are based on existing exception classes – we will see some examples of this in a later chapter.
Something we may want to do is raise an exception that we have just intercepted – perhaps because we want to handle it partially in the current function, but also want to respond to it in the code which called the function:
try: age = int(input("Please enter your age: ")) except ValueError as err: print("You entered incorrect age input: %s" % err) raise err
Exercise 3¶
- Rewrite the program from the first question of exercise 2 so that it prints the text of Python’s original exception inside the
except
clause instead of a custom message. - Rewrite the program from the second question of exercise 2 so that the exception which is caught in the
except
clause is re-raised after the error message is printed.
Debugging programs¶
Syntax errors are usually quite straightforward to debug: the error message shows us the line in the file where the error is, and it should be easy to find it and fix it.
Runtime errors can be a little more difficult to debug: the error message and the traceback can tell us exactly where the error occurred, but that doesn’t necessarily tell us what the problem is. Sometimes they are caused by something obvious, like an incorrect identifier name, but sometimes they are triggered by a particular state of the program – it’s not always clear which of many variables has an unexpected value.
Logical errors are the most difficult to fix because they don’t cause any errors that can be traced to a particular line in the code. All that we know is that the code is not behaving as it should be – sometimes tracking down the area of the code which is causing the incorrect behaviour can take a long time.
It is important to test your code to make sure that it behaves the way that you expect. A quick and simple way of testing that a function is doing the right thing, for example, is to insert a print statement after every line which outputs the intermediate results which were calculated on that line. Most programmers intuitively do this as they are writing a function, or perhaps if they need to figure out why it isn’t doing the right thing:
def hypotenuse(x, y): print("x is %f and y is %f" % (x, y)) x_2 = x**2 print(x_2) y_2 = y**2 print(y_2) z_2 = x_2 + y_2 print(z_2) z = math.sqrt(z_2) print(z) return z
This is a quick and easy thing to do, and even experienced programmers are guilty of doing it every now and then, but this approach has several disadvantages:
-
As soon as the function is working, we are likely to delete all the print statements, because we don’t want our program to print all this debugging information all the time. The problem is that code often changes – the next time we want to test this function we will have to add the print statements all over again.
-
To avoid rewriting the print statements if we happen to need them again, we may be tempted to comment them out instead of deleting them – leaving them to clutter up our code, and possibly become so out of sync that they end up being completely useless anyway.
-
To print out all these intermediate values, we had to spread out the formula inside the function over many lines. Sometimes it is useful to break up a calculation into several steps, if it is very long and putting it all on one line makes it hard to read, but sometimes it just makes our code unnecessarily verbose. Here is what the function above would normally look like:
def hypotenuse(x, y): return math.sqrt(x**2 + y**2)
How can we do this better? If we want to inspect the values of variables at various steps of a program’s execution, we can use a tool like pdb
. If we want our program to print out informative messages, possibly to a file, and we want to be able to control the level of detail at runtime without having to change anything in the code, we can use logging.
Most importantly, to check that our code is working correctly now and will keep working correctly, we should write a permanent suite of tests which we can run on our code regularly. We will discuss testing in more detail in a later chapter.
Logging¶
Sometimes it is valuable for a program to output messages to a console or a file as it runs. These messages can be used as a record of the program’s execution, and help us to find errors. Sometimes a bug occurs intermittently, and we don’t know what triggers it – if we only add debugging output to our program when we want to begin an active search for the bug, we may be unable to reproduce it. If our program logs messages to a file all the time, however, we may find that some helpful information has been recorded when we check the log after the bug has occurred.
Some kinds of messages are more important than others – errors are noteworthy events which should almost always be logged. Messages which record that an operation has been completed successfully may sometimes be useful, but are not as important as errors. Detailed messages which debug every step of a calculation can be interesting if we are trying to debug the calculation, but if they were printed all the time they would fill the console with noise (or make our log file really, really big).
We can use Python’s logging
module to add logging to our program in an easy and consistent way. Logging statements are almost like print statements, but whenever we log a message we specify a level for the message. When we run our program, we set a desired log level for the program. Only messages which have a level greater than or equal to the level which we have set will appear in the log. This means that we can temporarily switch on detailed logging and switch it off again just by changing the log level in one place.
There is a consistent set of logging level names which most languages use. In order, from the highest value (most severe) to the lowest value (least severe), they are:
- CRITICAL – for very serious errors
- ERROR – for less serious errors
- WARNING – for warnings
- INFO – for important informative messages
- DEBUG – for detailed debugging messages
These names are used for integer constants defined in the logging
module. The module also provides methods which we can use to log messages. By default these messages are printed to the console, and the default log level is WARNING
. We can configure the module to customise its behaviour – for example, we can write the messages to a file instead, raise or lower the log level and change the message format. Here is a simple logging example:
import logging # log messages to a file, ignoring anything less severe than ERROR logging.basicConfig(filename='myprogram.log', level=logging.ERROR) # these messages should appear in our file logging.error("The washing machine is leaking!") logging.critical("The house is on fire!") # but these ones won't logging.warning("We're almost out of milk.") logging.info("It's sunny today.") logging.debug("I had eggs for breakfast.")
There’s also a special exception
method which is used for logging exceptions. The level used for these messages is ERROR
, but additional information about the exception is added to them. This method is intended to be used inside exception handlers instead of error
:
try: age = int(input("How old are you? ")) except ValueError as err: logging.exception(err)
If we have a large project, we may want to set up a more complicated system for logging – perhaps we want to format certain messages differently, log different messages to different files, or log to multiple locations at the same time. The logging module also provides us with logger and handler objects for this purpose. We can use multiple loggers to create our messages, customising each one independently. Different handlers are associated with different logging locations. We can connect up our loggers and handlers in any way we like – one logger can use many handlers, and multiple loggers can use the same handler.
Exercise 4¶
- Write logging configuration for a program which logs to a file called
log.txt
and discards all logs less important thanINFO
. - Rewrite the second program from exercise 2 so that it uses this logging configuration instead of printing messages to the console (except for the first print statement, which is the purpose of the function).
- Do the same with the third program from exercise 2.
Answers to exercises¶
Answer to exercise 1¶
-
There are five syntax errors:
- Missing
def
keyword in function definition else
clause without anif
- Missing colon after
if
condition - Spelling mistake (“esle”)
- The
if
block is empty because theprint
statement is not indented correctly
- Missing
-
- The values entered by the user may not be valid integers or floating-point numbers.
- The user may enter zero for the divisor.
- If the
math
library hasn’t been imported,math.round
is undefined.
-
a
,b
andmy_list
need to be defined before this snippet.- The attempt to access the list element with index
x
may fail during one of the loop iterations if the range froma
tob
exceeds the size ofmy_list
. - The string formatting operation inside the
print
statement expectsmy_list[x]
to be a tuple with three numbers. If it has too many or too few elements, or isn’t a tuple at all, the attempt to format the string will fail.
-
- If you are accumulating a number total by multiplication, not addition, you need to initialise the total to
1
, not0
, otherwise the product will always be zero! - The line which adds
i_sq
tosum_squares
is not aligned correctly, and will only add the last value ofi_sq
after the loop has concluded. - The wrong variable is used: at each loop iteration the current number in the range is added to itself and
nums
remains unchanged.
- If you are accumulating a number total by multiplication, not addition, you need to initialise the total to
Answer to exercise 2¶
-
Here is an example program:
person = {} properties = [ ("name", str), ("surname", str), ("age", int), ("height", float), ("weight", float), ] for property, p_type in properties: valid_value = None while valid_value is None: try: value = input("Please enter your %s: " % property) valid_value = p_type(value) except ValueError: print("Could not convert %s '%s' to type %s. Please try again." % (property, value, p_type.__name__)) person[property] = valid_value
-
Here is an example program:
def print_list_element(thelist, index): try: print(thelist[index]) except IndexError: print("The list has no element at index %d." % index)
-
Here is an example program:
def add_to_list_in_dict(thedict, listname, element): try: l = thedict[listname] except KeyError: thedict[listname] = [] print("Created %s." % listname) else: print("%s already has %d elements." % (listname, len(l))) finally: thedict[listname].append(element) print("Added %s to %s." % (element, listname))
Answer to exercise 3¶
-
Here is an example program:
person = {} properties = [ ("name", str), ("surname", str), ("age", int), ("height", float), ("weight", float), ] for property, p_type in properties: valid_value = None while valid_value is None: try: value = input("Please enter your %s: " % property) valid_value = p_type(value) except ValueError as ve: print(ve) person[property] = valid_value
-
Here is an example program:
def print_list_element(thelist, index): try: print(thelist[index]) except IndexError as ie: print("The list has no element at index %d." % index) raise ie
Answer to exercise 4¶
-
Here is an example of the logging configuration:
import logging logging.basicConfig(filename='log.txt', level=logging.INFO)
-
Here is an example program:
def print_list_element(thelist, index): try: print(thelist[index]) except IndexError: logging.error("The list has no element at index %d." % index)
-
Here is an example program:
def add_to_list_in_dict(thedict, listname, element): try: l = thedict[listname] except KeyError: thedict[listname] = [] logging.info("Created %s." % listname) else: logging.info("%s already has %d elements." % (listname, len(l))) finally: thedict[listname].append(element) logging.info("Added %s to %s." % (element, listname))
Содержание:развернуть
- Как устроен механизм исключений
- Как обрабатывать исключения в Python (try except)
-
As — сохраняет ошибку в переменную
-
Finally — выполняется всегда
-
Else — выполняется когда исключение не было вызвано
-
Несколько блоков except
-
Несколько типов исключений в одном блоке except
-
Raise — самостоятельный вызов исключений
-
Как пропустить ошибку
- Исключения в lambda функциях
- 20 типов встроенных исключений в Python
- Как создать свой тип Exception
Программа, написанная на языке Python, останавливается сразу как обнаружит ошибку. Ошибки могут быть (как минимум) двух типов:
- Синтаксические ошибки — возникают, когда написанное выражение не соответствует правилам языка (например, написана лишняя скобка);
- Исключения — возникают во время выполнения программы (например, при делении на ноль).
Синтаксические ошибки исправить просто (если вы используете IDE, он их подсветит). А вот с исключениями всё немного сложнее — не всегда при написании программы можно сказать возникнет или нет в данном месте исключение. Чтобы приложение продолжило работу при возникновении проблем, такие ошибки нужно перехватывать и обрабатывать с помощью блока try/except
.
Как устроен механизм исключений
В Python есть встроенные исключения, которые появляются после того как приложение находит ошибку. В этом случае текущий процесс временно приостанавливается и передает ошибку на уровень вверх до тех пор, пока она не будет обработано. Если ошибка не будет обработана, программа прекратит свою работу (а в консоли мы увидим Traceback с подробным описанием ошибки).
💁♂️ Пример: напишем скрипт, в котором функция ожидает число, а мы передаём сроку (это вызовет исключение «TypeError»):
def b(value):
print("-> b")
print(value + 1) # ошибка тут
def a(value):
print("-> a")
b(value)
a("10")
> -> a
> -> b
> Traceback (most recent call last):
> File "test.py", line 11, in <module>
> a("10")
> File "test.py", line 8, in a
> b(value)
> File "test.py", line 3, in b
> print(value + 1)
> TypeError: can only concatenate str (not "int") to str
В данном примере мы запускаем файл «test.py» (через консоль). Вызывается функция «a«, внутри которой вызывается функция «b«. Все работает хорошо до сточки print(value + 1)
. Тут интерпретатор понимает, что нельзя конкатенировать строку с числом, останавливает выполнение программы и вызывает исключение «TypeError».
Далее ошибка передается по цепочке в обратном направлении: «b» → «a» → «test.py«. Так как в данном примере мы не позаботились обработать эту ошибку, вся информация по ошибке отобразится в консоли в виде Traceback.
Traceback (трассировка) — это отчёт, содержащий вызовы функций, выполненные в определенный момент. Трассировка помогает узнать, что пошло не так и в каком месте это произошло.
Traceback лучше читать снизу вверх ↑
В нашем примере Traceback
содержится следующую информацию (читаем снизу вверх):
TypeError
— тип ошибки (означает, что операция не может быть выполнена с переменной этого типа);can only concatenate str (not "int") to str
— подробное описание ошибки (конкатенировать можно только строку со строкой);- Стек вызова функций (1-я линия — место, 2-я линия — код). В нашем примере видно, что в файле «test.py» на 11-й линии был вызов функции «a» со строковым аргументом «10». Далее был вызов функции «b».
print(value + 1)
это последнее, что было выполнено — тут и произошла ошибка. most recent call last
— означает, что самый последний вызов будет отображаться последним в стеке (в нашем примере последним выполнилсяprint(value + 1)
).
В Python ошибку можно перехватить, обработать, и продолжить выполнение программы — для этого используется конструкция try ... except ...
.
Как обрабатывать исключения в Python (try except)
В Python исключения обрабатываются с помощью блоков try/except
. Для этого операция, которая может вызвать исключение, помещается внутрь блока try
. А код, который должен быть выполнен при возникновении ошибки, находится внутри except
.
Например, вот как можно обработать ошибку деления на ноль:
try:
a = 7 / 0
except:
print('Ошибка! Деление на 0')
Здесь в блоке try
находится код a = 7 / 0
— при попытке его выполнить возникнет исключение и выполнится код в блоке except
(то есть будет выведено сообщение «Ошибка! Деление на 0»). После этого программа продолжит свое выполнение.
💭 PEP 8 рекомендует, по возможности, указывать конкретный тип исключения после ключевого слова except
(чтобы перехватывать и обрабатывать конкретные исключения):
try:
a = 7 / 0
except ZeroDivisionError:
print('Ошибка! Деление на 0')
Однако если вы хотите перехватывать все исключения, которые сигнализируют об ошибках программы, используйте тип исключения Exception
:
try:
a = 7 / 0
except Exception:
print('Любая ошибка!')
As — сохраняет ошибку в переменную
Перехваченная ошибка представляет собой объект класса, унаследованного от «BaseException». С помощью ключевого слова as
можно записать этот объект в переменную, чтобы обратиться к нему внутри блока except
:
try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(e)
> [Errno 2] No such file or directory: 'ok123.txt'
В примере выше мы обращаемся к объекту класса «FileNotFoundError» (при выводе на экран через print
отобразится строка с полным описанием ошибки).
У каждого объекта есть поля, к которым можно обращаться (например если нужно логировать ошибку в собственном формате):
import datetime
now = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")
try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(f"{now} [FileNotFoundError]: {e.strerror}, filename: {e.filename}")
> 20-11-2021 18:42:01 [FileNotFoundError]: No such file or directory, filename: ok123.txt
Finally — выполняется всегда
При обработке исключений можно после блока try
использовать блок finally
. Он похож на блок except
, но команды, написанные внутри него, выполняются обязательно. Если в блоке try
не возникнет исключения, то блок finally
выполнится так же, как и при наличии ошибки, и программа возобновит свою работу.
Обычно try/except
используется для перехвата исключений и восстановления нормальной работы приложения, а try/finally
для того, чтобы гарантировать выполнение определенных действий (например, для закрытия внешних ресурсов, таких как ранее открытые файлы).
В следующем примере откроем файл и обратимся к несуществующей строке:
file = open('ok.txt', 'r')
try:
lines = file.readlines()
print(lines[5])
finally:
file.close()
if file.closed:
print("файл закрыт!")
> файл закрыт!
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> print(lines[5])
> IndexError: list index out of range
Даже после исключения «IndexError», сработал код в секции finally
, который закрыл файл.
p.s. данный пример создан для демонстрации, в реальном проекте для работы с файлами лучше использовать менеджер контекста with.
Также можно использовать одновременно три блока try/except/finally
. В этом случае:
- в
try
— код, который может вызвать исключения; - в
except
— код, который должен выполниться при возникновении исключения; - в
finally
— код, который должен выполниться в любом случае.
def sum(a, b):
res = 0
try:
res = a + b
except TypeError:
res = int(a) + int(b)
finally:
print(f"a = {a}, b = {b}, res = {res}")
sum(1, "2")
> a = 1, b = 2, res = 3
Else — выполняется когда исключение не было вызвано
Иногда нужно выполнить определенные действия, когда код внутри блока try
не вызвал исключения. Для этого используется блок else
.
Допустим нужно вывести результат деления двух чисел и обработать исключения в случае попытки деления на ноль:
b = int(input('b = '))
c = int(input('c = '))
try:
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
else:
print(f"a = {a}")
> b = 10
> c = 1
> a = 10.0
В этом случае, если пользователь присвоит переменной «с» ноль, то появится исключение и будет выведено сообщение «‘Ошибка! Деление на 0′», а код внутри блока else
выполняться не будет. Если ошибки не будет, то на экране появятся результаты деления.
Несколько блоков except
В программе может возникнуть несколько исключений, например:
- Ошибка преобразования введенных значений к типу
float
(«ValueError»); - Деление на ноль («ZeroDivisionError»).
В Python, чтобы по-разному обрабатывать разные типы ошибок, создают несколько блоков except
:
try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
except ValueError:
print('Число введено неверно')
else:
print(f"a = {a}")
> b = 10
> c = 0
> Ошибка! Деление на 0
> b = 10
> c = питон
> Число введено неверно
Теперь для разных типов ошибок есть свой обработчик.
Несколько типов исключений в одном блоке except
Можно также обрабатывать в одном блоке except сразу несколько исключений. Для этого они записываются в круглых скобках, через запятую сразу после ключевого слова except
. Чтобы обработать сообщения «ZeroDivisionError» и «ValueError» в одном блоке записываем их следующим образом:
try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except (ZeroDivisionError, ValueError) as er:
print(er)
else:
print('a = ', a)
При этом переменной er
присваивается объект того исключения, которое было вызвано. В результате на экран выводятся сведения о конкретной ошибке.
Raise — самостоятельный вызов исключений
Исключения можно генерировать самостоятельно — для этого нужно запустить оператор raise
.
min = 100
if min > 10:
raise Exception('min must be less than 10')
> Traceback (most recent call last):
> File "test.py", line 3, in <module>
> raise Exception('min value must be less than 10')
> Exception: min must be less than 10
Перехватываются такие сообщения точно так же, как и остальные:
min = 100
try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')
> Моя ошибка
Кроме того, ошибку можно обработать в блоке except
и пробросить дальше (вверх по стеку) с помощью raise
:
min = 100
try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')
raise
> Моя ошибка
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> raise Exception('min must be less than 10')
> Exception: min must be less than 10
Как пропустить ошибку
Иногда ошибку обрабатывать не нужно. В этом случае ее можно пропустить с помощью pass
:
try:
a = 7 / 0
except ZeroDivisionError:
pass
Исключения в lambda функциях
Обрабатывать исключения внутри lambda функций нельзя (так как lambda записывается в виде одного выражения). В этом случае нужно использовать именованную функцию.
20 типов встроенных исключений в Python
Иерархия классов для встроенных исключений в Python выглядит так:
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
ArithmeticError
AssertionError
...
...
...
ValueError
Warning
Все исключения в Python наследуются от базового BaseException
:
SystemExit
— системное исключение, вызываемое функциейsys.exit()
во время выхода из приложения;KeyboardInterrupt
— возникает при завершении программы пользователем (чаще всего при нажатии клавиш Ctrl+C);GeneratorExit
— вызывается методомclose
объектаgenerator
;Exception
— исключения, которые можно и нужно обрабатывать (предыдущие были системными и их трогать не рекомендуется).
От Exception
наследуются:
1 StopIteration
— вызывается функцией next в том случае если в итераторе закончились элементы;
2 ArithmeticError
— ошибки, возникающие при вычислении, бывают следующие типы:
FloatingPointError
— ошибки при выполнении вычислений с плавающей точкой (встречаются редко);OverflowError
— результат вычислений большой для текущего представления (не появляется при операциях с целыми числами, но может появиться в некоторых других случаях);ZeroDivisionError
— возникает при попытке деления на ноль.
3 AssertionError
— выражение, используемое в функции assert
неверно;
4 AttributeError
— у объекта отсутствует нужный атрибут;
5 BufferError
— операция, для выполнения которой требуется буфер, не выполнена;
6 EOFError
— ошибка чтения из файла;
7 ImportError
— ошибка импортирования модуля;
8 LookupError
— неверный индекс, делится на два типа:
IndexError
— индекс выходит за пределы диапазона элементов;KeyError
— индекс отсутствует (для словарей, множеств и подобных объектов);
9 MemoryError
— память переполнена;
10 NameError
— отсутствует переменная с данным именем;
11 OSError
— исключения, генерируемые операционной системой:
ChildProcessError
— ошибки, связанные с выполнением дочернего процесса;ConnectionError
— исключения связанные с подключениями (BrokenPipeError, ConnectionResetError, ConnectionRefusedError, ConnectionAbortedError);FileExistsError
— возникает при попытке создания уже существующего файла или директории;FileNotFoundError
— генерируется при попытке обращения к несуществующему файлу;InterruptedError
— возникает в том случае если системный вызов был прерван внешним сигналом;IsADirectoryError
— программа обращается к файлу, а это директория;NotADirectoryError
— приложение обращается к директории, а это файл;PermissionError
— прав доступа недостаточно для выполнения операции;ProcessLookupError
— процесс, к которому обращается приложение не запущен или отсутствует;TimeoutError
— время ожидания истекло;
12 ReferenceError
— попытка доступа к объекту с помощью слабой ссылки, когда объект не существует;
13 RuntimeError
— генерируется в случае, когда исключение не может быть классифицировано или не подпадает под любую другую категорию;
14 NotImplementedError
— абстрактные методы класса нуждаются в переопределении;
15 SyntaxError
— ошибка синтаксиса;
16 SystemError
— сигнализирует о внутренне ошибке;
17 TypeError
— операция не может быть выполнена с переменной этого типа;
18 ValueError
— возникает когда в функцию передается объект правильного типа, но имеющий некорректное значение;
19 UnicodeError
— исключение связанное с кодирование текста в unicode
, бывает трех видов:
UnicodeEncodeError
— ошибка кодирования;UnicodeDecodeError
— ошибка декодирования;UnicodeTranslateError
— ошибка переводаunicode
.
20 Warning
— предупреждение, некритическая ошибка.
💭 Посмотреть всю цепочку наследования конкретного типа исключения можно с помощью модуля inspect
:
import inspect
print(inspect.getmro(TimeoutError))
> (<class 'TimeoutError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
📄 Подробное описание всех классов встроенных исключений в Python смотрите в официальной документации.
Как создать свой тип Exception
В Python можно создавать свои исключения. При этом есть одно обязательное условие: они должны быть потомками класса Exception
:
class MyError(Exception):
def __init__(self, text):
self.txt = text
try:
raise MyError('Моя ошибка')
except MyError as er:
print(er)
> Моя ошибка
С помощью try/except
контролируются и обрабатываются ошибки в приложении. Это особенно актуально для критически важных частей программы, где любые «падения» недопустимы (или могут привести к негативным последствиям). Например, если программа работает как «демон», падение приведет к полной остановке её работы. Или, например, при временном сбое соединения с базой данных, программа также прервёт своё выполнение (хотя можно было отловить ошибку и попробовать соединиться в БД заново).
Вместе с try/except
можно использовать дополнительные блоки. Если использовать все блоки описанные в статье, то код будет выглядеть так:
try:
# попробуем что-то сделать
except (ZeroDivisionError, ValueError) as e:
# обрабатываем исключения типа ZeroDivisionError или ValueError
except Exception as e:
# исключение не ZeroDivisionError и не ValueError
# поэтому обрабатываем исключение общего типа (унаследованное от Exception)
# сюда не сходят исключения типа GeneratorExit, KeyboardInterrupt, SystemExit
else:
# этот блок выполняется, если нет исключений
# если в этом блоке сделать return, он не будет вызван, пока не выполнился блок finally
finally:
# этот блок выполняется всегда, даже если нет исключений else будет проигнорирован
# если в этом блоке сделать return, то return в блоке
Подробнее о работе с исключениями в Python можно ознакомиться в официальной документации.
При выполнении заданий к главам вы скорее всего нередко сталкивались с возникновением различных ошибок. На этой главе мы изучим подход, который позволяет обрабатывать ошибки после их возникновения.
Напишем программу, которая будет считать обратные значения для целых чисел из заданного диапазона и выводить их в одну строку с разделителем «;». Один из вариантов кода для решения этой задачи выглядит так:
print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
Программа получилась в одну строчку за счёт использования списочных выражений. Однако при вводе диапазона чисел, включающем в себя 0 (например, от -1 до 1), программа выдаст следующую ошибку:
ZeroDivisionError: division by zero
В программе произошла ошибка «деление на ноль». Такая ошибка, возникающая при выполнении программы и останавливающая её работу, называется исключением.
Попробуем в нашей программе избавиться от возникновения исключения деления на ноль. Пусть при попадании 0 в диапазон чисел, обработка не производится и выводится сообщение «Диапазон чисел содержит 0». Для этого нужно проверить до списочного выражения наличие нуля в диапазоне:
interval = range(int(input()), int(input()) + 1)
if 0 in interval:
print("Диапазон чисел содержит 0.")
else:
print(";".join(str(1 / x) for x in interval))
Теперь для диапазона, включающего в себя 0, например, от -2 до 2, исключения ZeroDivisionError
не возникнет. Однако при вводе строки, которую невозможно преобразовать в целое число (например, «a»), будет вызвано другое исключение:
ValueError: invalid literal for int() with base 10: 'a'
Произошло исключение ValueError
. Для борьбы с этой ошибкой нам придётся проверить, что строка состоит только из цифр. Сделать это нужно до преобразования в число. Тогда наша программа будет выглядеть так:
start = input()
end = input()
# Метод lstrip("-"), удаляющий символы "-" в начале строки, нужен для учёта
# отрицательных чисел, иначе isdigit() вернёт для них False
if not (start.lstrip("-").isdigit() and end.lstrip("-").isdigit()):
print("Необходимо ввести два числа.")
else:
interval = range(int(start), int(end) + 1)
if 0 in interval:
print("Диапазон чисел содержит 0.")
else:
print(";".join(str(1 / x) for x in interval))
Теперь наша программа работает без ошибок и при вводе строк, которые нельзя преобразовать в целое число.
Подход, который был нами применён для предотвращения ошибок, называется «Look Before You Leap» (LBYL), или «посмотри перед прыжком». В программе, реализующей такой подход, проверяются возможные условия возникновения ошибок до исполнения основного кода.
Подход LBYL имеет недостатки. Программу из примера стало сложнее читать из-за вложенного условного оператора. Проверка условия, что строка может быть преобразована в число, выглядит даже сложнее, чем списочное выражение. Вложенный условный оператор не решает поставленную задачу, а только лишь проверяет входные данные на корректность. Легко заметить, что решение основной задачи заняло меньше времени, чем составление условий проверки корректности входных данных.
Существует другой подход для работы с ошибками: «Easier to Ask Forgiveness than Permission» (EAFP) или «проще извиниться, чем спрашивать разрешение». В этом подходе сначала исполняется код, а в случае возникновения ошибок происходит их обработка. Подход EAFP реализован в Python в виде обработки исключений.
Исключения в Python являются классами ошибок. В Python есть много стандартных исключений. Они имеют определённую иерархию за счёт механизма наследования классов. В документации Python версии 3.10.8 приводится следующее дерево иерархии стандартных исключений:
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 +-- EncodingWarning +-- ResourceWarning
Для обработки исключения в Python используется следующий синтаксис:
try: <код , который может вызвать исключения при выполнении> except <классисключения_1>: <код обработки исключения> except <классисключения_2>: <код обработки исключения> ... else: <код выполняется, если не вызвано исключение в блоке try> finally: <код , который выполняется всегда>
Блок try
содержит код, в котором нужно обработать исключения, если они возникнут. При возникновении исключения интерпретатор последовательно проверяет в каком из блоков except
обрабатывается это исключение. Исключение обрабатывается в первом блоке except
, обрабатывающем класс этого исключения или базовый класс возникшего исключения. Необходимо учитывать иерархию исключений для определения порядка их обработки в блоках except
. Начинать обработку исключений следует с более узких классов исключений. Если начать с более широкого класса исключения, например, Exception
, то всегда при возникновении исключения будет срабатывать первый блок except
. Сравните два следующих примера. В первом порядок обработки исключений указан от производных классов к базовым, а во втором – наоборот.
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
При вводе значений «0» и «a» получим ожидаемый соответствующий возникающим исключениям вывод:
Невозможно преобразовать строку в число.
и
Ошибка деления на ноль.
Второй пример:
try:
print(1 / int(input()))
except Exception:
print("Неизвестная ошибка.")
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
При вводе значений «0» и «a» получим в обоих случаях неинформативный вывод:
Неизвестная ошибка.
Необязательный блок else
выполняет код в случае, если в блоке try
не вызвано исключение. Добавим блок else
в пример для вывода сообщения об успешном выполнении операции:
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
else:
print("Операция выполнена успешно.")
Теперь при вводе корректного значения, например, «5», вывод программы будет следующим:
2.0 Операция выполнена успешно.
Блок finally
выполняется всегда, даже если возникло какое-то исключение, не учтённое в блоках except
или код в этих блоках сам вызвал какое-либо исключение. Добавим в нашу программу вывод строки «Программа завершена» в конце программы даже при возникновении исключений:
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
else:
print("Операция выполнена успешно.")
finally:
print("Программа завершена.")
Перепишем код, созданный с применением подхода LBYL, для первого примера из этой главы с использованием обработки исключений:
try:
print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
except ZeroDivisionError:
print("Диапазон чисел содержит 0.")
except ValueError:
print("Необходимо ввести два числа.")
Теперь наша программа читается намного легче. При этом создание кода для обработки исключений не заняло много времени и не потребовало проверки сложных условий.
Исключения можно принудительно вызывать с помощью оператора raise
. Этот оператор имеет следующий синтаксис:
raise <класс исключения>(параметры)
В качестве параметра можно, например, передать строку с сообщением об ошибке.
В Python можно создавать свои собственные исключения. Синтаксис создания исключения такой же, как и у создания класса. При создании исключения его необходимо наследовать от какого-либо стандартного класса-исключения.
Напишем программу, которая выводит сумму списка целых чисел, и вызывает исключение, если в списке чисел есть хотя бы одно чётное или отрицательное число. Создадим свои классы исключений:
- NumbersError – базовый класс исключения;
- EvenError – исключение, которое вызывается при наличии хотя бы одного чётного числа;
- NegativeError – исключение, которое вызывается при наличии хотя бы одного отрицательного числа.
class NumbersError(Exception):
pass
class EvenError(NumbersError):
pass
class NegativeError(NumbersError):
pass
def no_even(numbers):
if all(x % 2 != 0 for x in numbers):
return True
raise EvenError("В списке не должно быть чётных чисел")
def no_negative(numbers):
if all(x >= 0 for x in numbers):
return True
raise NegativeError("В списке не должно быть отрицательных чисел")
def main():
print("Введите числа в одну строку через пробел:")
try:
numbers = [int(x) for x in input().split()]
if no_negative(numbers) and no_even(numbers):
print(f"Сумма чисел равна: {sum(numbers)}.")
except NumbersError as e: # обращение к исключению как к объекту
print(f"Произошла ошибка: {e}.")
except Exception as e:
print(f"Произошла непредвиденная ошибка: {e}.")
if __name__ == "__main__":
main()
Обратите внимание: в программе основной код выделен в функцию main
. А код вне функций содержит только условный оператор и вызов функции main
при выполнении условия __name__ == "__main__"
. Это условие проверяет, запущен ли файл как самостоятельная программа или импортирован как модуль.
Любая программа, написанная на языке программирования Python может быть импортирована как модуль в другую программу. В идеологии Python импортировать модуль – значит полностью его выполнить. Если основной код модуля содержит вызовы функций, ввод или вывод данных без использования указанного условия __name__ == "__main__"
, то произойдёт полноценный запуск программы. А это не всегда удобно, если из модуля нужна только отдельная функция или какой-либо класс.
При изучении модуля itertools
, мы говорили о том, как импортировать модуль в программу. Покажем ещё раз два способа импорта на примере собственного модуля.
Для импорта модуля из файла, например example_module.py
, нужно указать его имя, если он находится в той же папке, что и импортирующая его программа:
import example_module
Если требуется отдельный компонент модуля, например функция или класс, то импорт можно осуществить так:
from example_module import some_function, ExampleClass
Обратите внимание: при втором способе импортированные объекты попадают в пространство имён новой программы. Это означает, что они будут объектами новой программы, и в программе не должно быть других объектов с такими же именами.
Время прочтения
10 мин
Просмотры 31K
Одним из недостатков гибких языков, таких как 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
— РЕГИСТРАЦИЯ
In this article, you will learn error and exception handling in Python.
By the end of the article, you’ll know:
- How to handle exceptions using the try, except, and finally statements
- How to create a custom exception
- How to raise an exceptions
- How to use built-in exception effectively to build robust Python programs
Table of contents
- What are Exceptions?
- Why use Exception
- What are Errors?
- Syntax error
- Logical errors (Exception)
- Built-in Exceptions
- The try and except Block to Handling Exceptions
- Catching Specific Exceptions
- Handle multiple exceptions with a single except clause
- Using try with finally
- Using try with else clause
- Raising an Exceptions
- Exception Chaining
- Custom and User-defined Exceptions
- Customizing Exception Classes
- Exception Lifecycle
- Warnings
What are Exceptions?
An exception is an event that occurs during the execution of programs that disrupt the normal flow of execution (e.g., KeyError Raised when a key is not found in a dictionary.) An exception is a Python object that represents an error..
In Python, an exception is an object derives from the BaseException class that contains information about an error event that occurred within a method. Exception object contains:
- Error type (exception name)
- The state of the program when the error occurred
- An error message describes the error event.
Exception are useful to indicate different types of possible failure condition.
For example, bellow are the few standard exceptions
- FileNotFoundException
- ImportError
- RuntimeError
- NameError
- TypeError
In Python, we can throw an exception in the try block and catch it in except block.
Why use Exception
- Standardized error handling: Using built-in exceptions or creating a custom exception with a more precise name and description, you can adequately define the error event, which helps you debug the error event.
- Cleaner code: Exceptions separate the error-handling code from regular code, which helps us to maintain large code easily.
- Robust application: With the help of exceptions, we can develop a solid application, which can handle error event efficiently
- Exceptions propagation: By default, the exception propagates the call stack if you don’t catch it. For example, if any error event occurred in a nested function, you do not have to explicitly catch-and-forward it; automatically, it gets forwarded to the calling function where you can handle it.
- Different error types: Either you can use built-in exception or create your custom exception and group them by their generalized parent class, or Differentiate errors by their actual class
What are Errors?
On the other hand, An error is an action that is incorrect or inaccurate. For example, syntax error. Due to which the program fails to execute.
The errors can be broadly classified into two types:
- Syntax errors
- Logical errors
Syntax error
The syntax error occurs when we are not following the proper structure or syntax of the language. A syntax error is also known as a parsing error.
When Python parses the program and finds an incorrect statement it is known as a syntax error. When the parser found a syntax error it exits with an error message without running anything.
Common Python Syntax errors:
- Incorrect indentation
- Missing colon, comma, or brackets
- Putting keywords in the wrong place.
Example
print("Welcome to PYnative")
print("Learn Python with us..")
Output
print("Learn Python with us..")
^
IndentationError: unexpected indent
Logical errors (Exception)
Even if a statement or expression is syntactically correct, the error that occurs at the runtime is known as a Logical error or Exception. In other words, Errors detected during execution are called exceptions.
Common Python Logical errors:
- Indenting a block to the wrong level
- using the wrong variable name
- making a mistake in a boolean expression
Example
a = 10
b = 20
print("Addition:", a + c)
Output
print("Addition:", a + c)
NameError: name 'c' is not defined
Built-in Exceptions
The below table shows different built-in exceptions.
Python automatically generates many exceptions and errors. Runtime exceptions, generally a result of programming errors, such as:
- Reading a file that is not present
- Trying to read data outside the available index of a list
- Dividing an integer value by zero
Exception | Description |
---|---|
AssertionError |
Raised when an assert statement fails. |
AttributeError |
Raised when attribute assignment or reference fails. |
EOFError |
Raised when the input() function hits the end-of-file condition. |
FloatingPointError |
Raised when a floating-point operation fails. |
GeneratorExit |
Raise when a generator’s close() method is called. |
ImportError |
Raised when the imported module is not found. |
IndexError |
Raised when the index of a sequence is out of range. |
KeyError |
Raised when a key is not found in a dictionary. |
KeyboardInterrupt |
Raised when the user hits the interrupt key (Ctrl+C or Delete) |
MemoryError |
Raised when an operation runs out of memory. |
NameError |
Raised when a variable is not found in the local or global scope. |
OSError |
Raised when system operation causes system related error. |
ReferenceError |
Raised when a weak reference proxy is used to access a garbage collected referent. |
Example: The FilenotfoundError
is raised when a file in not present on the disk
fp = open("test.txt", "r")
if fp:
print("file is opened successfully")
Output:
FileNotFoundError: [Errno 2] No such file or directory: 'test.txt'
The try
and except
Block to Handling Exceptions
When an exception occurs, Python stops the program execution and generates an exception message. It is highly recommended to handle exceptions. The doubtful code that may raise an exception is called risky code.
To handle exceptions we need to use try and except block. Define risky code that can raise an exception inside the try
block and corresponding handling code inside the except
block.
Syntax
try :
# statements in try block
except :
# executed when exception occured in try block
The try block is for risky code that can raise an exception and the except block to handle error raised in a try block. For example, if we divide any number by zero, try block will throw ZeroDivisionError
, so we should handle that exception in the except block.
When we do not use try…except
block in the program, the program terminates abnormally, or it will be nongraceful termination of the program.
Now let’s see the example when we do not use try…except
block for handling exceptions.
Example:
a = 10
b = 0
c = a / b
print("a/b = %d" % c)
Output
Traceback (most recent call last):
File "E:/demos/exception.py", line 3, in <module>
c = a / b
ZeroDivisionError: division by zero
We can see in the above code when we are divided by 0; Python throws an exception as ZeroDivisionError
and the program terminated abnormally.
We can handle the above exception using the try…except
block. See the following code.
Example
try:
a = 10
b = 0
c = a/b
print("The answer of a divide by b:", c)
except:
print("Can't divide with zero. Provide different number")
Output
Can't divide with zero. Provide different number
Catching Specific Exceptions
We can also catch a specific exception. In the above example, we did not mention any specific exception in the except block. Catch all the exceptions and handle every exception is not good programming practice.
It is good practice to specify an exact exception that the except clause should catch. For example, to catch an exception that occurs when the user enters a non-numerical value instead of a number, we can catch only the built-in ValueError exception that will handle such an event properly.
We can specify which exception except
block should catch or handle. A try
block can be followed by multiple numbers of except
blocks to handle the different exceptions. But only one exception will be executed when an exception occurs.
Example
In this example, we will ask the user for the denominator value. If the user enters a number, the program will evaluate and produce the result.
If the user enters a non-numeric value then, the try block will throw a ValueError
exception, and we can catch that using a first catch block ‘except ValueError’ by printing the message ‘Entered value is wrong’.
And suppose the user enters the denominator as zero. In that case, the try block will throw a ZeroDivisionError
, and we can catch that using a second catch block by printing the message ‘Can’t divide by zero’.
try:
a = int(input("Enter value of a:"))
b = int(input("Enter value of b:"))
c = a/b
print("The answer of a divide by b:", c)
except ValueError:
print("Entered value is wrong")
except ZeroDivisionError:
print("Can't divide by zero")
Output 1:
Enter value of a:Ten Entered value is wrong
Output 2:
Enter value of a:10 Enter value of b:0 Can't divide by zero
Output 3:
Enter value of a:10 Enter value of b:2 The answer of a divide by b: 5.0
Handle multiple exceptions with a single except clause
We can also handle multiple exceptions with a single except
clause. For that, we can use an tuple
of values to specify multiple exceptions in an except
clause.
Example
Let’s see how to specifiy two exceptions in the single except clause.
try:
a = int(input("Enter value of a:"))
b = int(input("Enter value of b:"))
c = a / b
print("The answer of a divide by b:", c)
except(ValueError, ZeroDivisionError):
print("Please enter a valid value")
Using try
with finally
Python provides the finally
block, which is used with the try block statement. The finally
block is used to write a block of code that must execute, whether the try
block raises an error or not.
Mainly, the finally
block is used to release the external resource. This block provides a guarantee of execution.
Clean-up actions using finally
Sometimes we want to execute some action at any cost, even if an error occurred in a program. In Python, we can perform such actions using a finally statement with a try and except statement.
The block of code written in the finally block will always execute even there is an exception in the try and except block.
If an exception is not handled by except clause, then finally block executes first, then the exception is thrown. This process is known as clean-up action.
Syntax
try:
# block of code
# this may throw an exception
finally:
# block of code
# this will always be executed
# after the try and any except block
Example
try:
a = int(input("Enter value of a:"))
b = int(input("Enter value of b:"))
c = a / b
print("The answer of a divide by b:", c)
except ZeroDivisionError:
print("Can't divide with zero")
finally:
print("Inside a finally block")
Output 1:
Enter value of a:20 Enter value of b:5 The answer of a divide by b: 4.0 Inside a finally block
Output 2:
Enter value of a:20 Enter value of b:0 Can't divide with zero Inside a finally block
In the above example, we can see we divide a number by 0 and get an error, and the program terminates normally. In this case, the finally
block was also executed.
Using try
with else
clause
Sometimes we might want to run a specific block of code. In that case, we can use else
block with the try-except
block. The else
block will be executed if and only if there are no exception is the try
block. For these cases, we can use the optional else
statement with the try
statement.
Why to use else
block with try?
Use else statemen with try block to check if try block executed without any exception or if you want to run a specific code only if an exception is not raised
Syntax
try:
# block of code
except Exception1:
# block of code
else:
# this code executes when exceptions not occured
try
: Thetry
block for risky code that can throw an exception.except
: Theexcept
block to handle error raised in atry
block.else
: Theelse
block is executed if there is no exception.
Example
try:
a = int(input("Enter value of a:"))
b = int(input("Enter value of b:"))
c = a / b
print("a/b = %d" % c)
except ZeroDivisionError:
print("Can't divide by zero")
else:
print("We are in else block ")
Output 1
Enter value of a: 20 Enter value of b:4 a/b = 5 We are in else block
Output 2
Enter value of a: 20 Enter value of b:0 Can't divide by zero
Raising an Exceptions
In Python, the raise
statement allows us to throw an exception. The single arguments in the raise
statement show an exception to be raised. This can be either an exception object or an Exception
class that is derived from the Exception
class.
The raise
statement is useful in situations where we need to raise an exception to the caller program. We can raise exceptions in cases such as wrong data received or any validation failure.
Follow the below steps to raise an exception:
- Create an exception of the appropriate type. Use the existing built-in exceptions or create your won exception as per the requirement.
- Pass the appropriate data while raising an exception.
- Execute a raise statement, by providing the exception class.
The syntax to use the raise
statement is given below.
raise Exception_class,<value>
Example
In this example, we will throw an exception if interest rate is greater than 100.
def simple_interest(amount, year, rate):
try:
if rate > 100:
raise ValueError(rate)
interest = (amount * year * rate) / 100
print('The Simple Interest is', interest)
return interest
except ValueError:
print('interest rate is out of range', rate)
print('Case 1')
simple_interest(800, 6, 8)
print('Case 2')
simple_interest(800, 6, 800)
Output:
Case 1 The Simple Interest is 384.0 Case 2 interest rate is out of range 800
Exception Chaining
The exception chaining is available only in Python 3. The raise
statements allow us as optional from
statement, which enables chaining exceptions. So we can implement exception chaining in python3 by using raise…from
clause to chain exception.
When exception raise, exception chaining happens automatically. The exception can be raised inside except
or finally
block section. We also disabled exception chaining by using from None
idiom.
Example
try:
a = int(input("Enter value of a:"))
b = int(input("Enter value of b:"))
c = a/b
print("The answer of a divide by b:", c)
except ZeroDivisionError as e:
raise ValueError("Division failed") from e
# Output: Enter value of a:10
# Enter value of b:0
# ValueError: Division failed
In the above example, we use exception chaining using raise...from
clause and raise ValueError
division failed.
Custom and User-defined Exceptions
Sometimes we have to define and raise
exceptions explicitly to indicate that something goes wrong. Such a type of exception is called a user-defined exception or customized exception.
The user can define custom exceptions by creating a new class. This new exception class has to derive either directly or indirectly from the built-in class Exception
. In Python, most of the built-in exceptions also derived from the Exception
class.
class Error(Exception):
"""Base class for other exceptions"""
pass
class ValueTooSmallError(Error):
"""Raised when the input value is small"""
pass
class ValueTooLargeError(Error):
"""Raised when the input value is large"""
pass
while(True):
try:
num = int(input("Enter any value in 10 to 50 range: "))
if num < 10:
raise ValueTooSmallError
elif num > 50:
raise ValueTooLargeError
break
except ValueTooSmallError:
print("Value is below range..try again")
except ValueTooLargeError:
print("value out of range...try again")
print("Great! value in correct range.")
Output
Enter any value in 10 to 50 range: 5
Value is below range..try again
Enter any value in 10 to 50 range: 60
value out of range...try again
Enter any value in 10 to 50 range: 11
Great! value in correct range.
In the above example, we create two custom classes or user-defined classes with names, ValueTooSmallError
and ValueTooLargeError
.When the entered value is below the range, it will raise ValueTooSmallError
and if the value is out of then, it will raise
ValueTooLargeError
.
Customizing Exception Classes
We can customize the classes by accepting arguments as per our requirements. Any custom exception class must be Extending from BaseException
class or subclass of BaseException
.
In the above example, we create a custom class that is inherited from the base class Exception
. This class takes one argument age. When entered age is negative, it will raise NegativeAgeError
.
class NegativeAgeError(Exception):
def __init__(self, age, ):
message = "Age should not be negative"
self.age = age
self.message = message
age = int(input("Enter age: "))
if age < 0:
raise NegativeAgeError(age)
# Output:
# raise NegativeAgeError(age)
# __main__.NegativeAgeError: -9
Output:
Enter age: -28 Traceback (most recent call last): File "E:/demos/exception.py", line 11, in raise NegativeAgeError(age) main.NegativeAgeError: -28
Done
Exception Lifecycle
- When an exception is raised, The runtime system attempts to find a handler for the exception by backtracking the ordered list of methods calls. This is known as the call stack.
- If a handler is found (i.e., if
except
block is located), there are two cases in theexcept
block; either exception is handled or possibly re-thrown. - If the handler is not found (the runtime backtracks to the method chain’s last calling method), the exception stack trace is printed to the standard error console, and the application stops its execution.
Example
def sum_of_list(numbers):
return sum(numbers)
def average(sum, n):
# ZeroDivisionError if list is empty
return sum / n
def final_data(data):
for item in data:
print("Average:", average(sum_of_list(item), len(item)))
list1 = [10, 20, 30, 40, 50]
list2 = [100, 200, 300, 400, 500]
# empty list
list3 = []
lists = [list1, list2, list3]
final_data(lists)
Output
Average: 30.0 Traceback (most recent call last): File "E:/demos/exceptions.py", line 17, in final_data(lists) File "E:/demos/exceptions.py", line 11, in final_data print("Average:", average(sum_of_list(item), len(item))) Average: 300.0 File "E:/demos/exceptions.py", line 6, in average return sum / n ZeroDivisionError: division by zero
The above stack trace shows the methods that are being called from main() until the method created an exception condition. It also shows line numbers.
Warnings
Several built-in exceptions represent warning categories. This categorization is helpful to be able to filter out groups of warnings.
The warning doesn’t stop the execution of a program it indicates the possible improvement
Below is the list of warning exception
Waring Class | Meaning |
---|---|
Warning | Base class for warning categories |
UserWarning | Base class for warnings generated by user code |
DeprecationWarning | Warnings about deprecated features |
PendingDeprecationWarning | Warnings about features that are obsolete and expected to be deprecated in the future, but are not deprecated at the moment. |
SyntaxWarning | Warnings about dubious syntax |
RuntimeWarning | Warnings about the dubious runtime behavior |
FutureWarning | Warnings about probable mistakes in module imports |
ImportWarning | Warnings about probable mistakes in module imports |
UnicodeWarning | Warnings related to Unicode data |
BytesWarning | Warnings related to bytes and bytearray. |
ResourceWarning | Warnings related to resource usage |