Разрабатывая приложения вам придется работать с файлами, анализировать большие объемы данных, сохранять пользовательские данные, чтобы они не терялись по завершению работы программы. Также при работе с файлами важно научиться обрабатывать ошибки, чтобы они не привели к аварийному завершению программы. Для этого в Python существуют специальные объекты — исключения, которые создаются для управления ошибок.
| Содержание страницы: |
|---|
| 1. Чтение файла |
| 1.2. Чтение больших файлов и работа с ними |
| 1.3. Анализ текста из файла |
| 2. Запись в файл |
| 2.1. Запись в пустой файл |
| 2.2. Многострочная запись в файл |
| 2.3. Присоединение данных к файлу |
| 3. Исключения |
| 3.1. Блоки try-except |
| 3.2. Блоки try-except-else |
| 3.3. Блоки try-except с текстовыми файлами |
| 3.4. Ошибки без уведомления пользователя |
1. Чтение файла в Python
В файлах может содержаться любой объем данных, начиная от небольшого рассказа и до сохранения истории погоды за столетия. Чтение файлов особенно актуально для приложений, предназначенных для анализа данных. Приведем пример простой программы, которая открывает файл и выводит его содержимое на экран. В примере я буду использовать файл с числом «Пи» с точностью до 10 знаков после запятой. Скачать этот файл можно прямо здесь ( pi_10.txt ) или самим создать текстовый файл и сохранить под любым именем. Пример программы, которая открывает файл и выводит содержимое на экран:
with open(‘pi_10.txt’) as file_pi:
digits = file_pi.read()
print(digits)
Код начинается с ключевого слова with. При использование ключевого слова with используемый файл открывается с помощью функции open(), а закрывается автоматически после завершения блока with и вам не придется в конце вызывать функцию close(). Файлы можно открывать и закрывать явными вызовами open() и close(). Функция open() получает один аргумент — имя открываемого файла, в нашем случае ‘pi_10.txt’. Python ищет указанный файл в каталоге, где хранится файл текущей программы. Функция open() возвращает объект, представляющий файл ‘pi_10.txt’. Python сохраняет этот объект в переменной file_pi .
После появления объекта, представляющего файл ‘pi_10.txt’, используется метод read(), который читает все содержимое файла и сохраняет его в одной строке в переменной contents. В конце с помощью функции print содержимое выводится на экран. Запустив этот файл, мы получим данные, находящиеся в нашем файле ‘pi_10.txt’.
3.1415926535
В случае, если файл расположен не в одном каталоге с файлом программы, необходимо указать путь, чтобы Python искал файлы в конкретном месте. Существует два пути как прописать расположение файла:
-
Относительный путь.
Относительный путь приказывает Python искать файлы в каталоге, который задается относительно каталога, в котором находится текущий файл программы
with open(‘files/имя_файла.txt’) as file:
-
Абсолютный путь.
Местонахождение файла не зависит от того, где находится ваша программа. Абсолютные пути обычно длиннее относительных, поэтому их лучше сохранить в переменную и затем передать функции open().
file_path = ‘/Users/Desktop/files/имя_файла.txt’
with open(file_path) as file:
С абсолютными путями можно читать файлы из любого каталога вашей системы.
1.2. Чтение больших файлов на Python и работа с ними
В первом примере был файл с 10 знаками после запятой. Теперь давайте проанализируем файл с миллионом знаков числа «Пи» после запятой. Скачать число «Пи» с миллионом знаков после запятой можно отсюда( ‘pi_1000000.txt’ ). Изменять код из первого примера не придется, просто заменим файл, который должен читать Python.
Выведем на экран первые 100 знаков после запятой. Добавим в конец функцию len, чтобы узнать длину файла
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679
1000002
Из выходных данных видно, что строка содержит значение «Пи» с точностью до 1 000 000 знаков после запятой. В Python нет никаких ограничений на длину данных, с которыми можно работать, единственное ограничение это объем памяти вашей системы.
После сохранения данных в переменной можно делать с ними все что угодно. Давайте проверим, входит ли в число «Пи» дата вашего дня рождения. Напишем небольшую программу, которая будет читать файл и проверять входит ли дата день рождения в первый миллион числа «Пи»:
with open(‘pi_1000000.txt‘) as file_pi:
digits = file_pi.read()
birthday = input(«Введите дату дня рождения: «)
if birthday in digits:
print(«Ваш день рождение входит в число ‘Пи'»)
else:
print(«Ваш день рождение не входит в число ‘Пи'»)
Начало программы не изменилось, читаем файл и сохраняем данные в переменной digits. Далее запрашиваем данные от пользователя с помощью функции input и сохраняем в переменную birstday. Затем проверяем вхождение birstday в digits с помощью команды if-else. Запустив несколько раз программу, получим результат:
Введите дату дня рождения: 260786
Ваш день рождение не входит в число ‘Пи’
Введите дату дня рождения: 260884
Ваш день рождение входит в число ‘Пи’
В зависимости от введенных данных мы получили результат вхождения или не вхождения дня рождения в число «Пи»
Важно: Читая данные из текстового файла, Python интерпретирует весь текст как строку. Если вы хотите работать с ним в числовом контексте, то преобразуйте данные в целое число функцией int() или в вещественное число функцией float().
1.3. Анализ текста из файла на Python
Python может анализировать текстовые файлы, содержащие целые книги. Возьмем книгу «Алиса в стране чудес» и попробуем подсчитать количество слов в книге. Текстовый файл с книгой можете скачать здесь(‘ alice ‘) или загрузить любое другое произведение. Напишем простую программу, которая подсчитает количество слов в книге и сколько раз повторяется имя Алиса в книге.
filename = ‘alice.txt’
with open(filename, encoding=’utf-8′) as file:
contents = file.read()
n_alice = contents.lower().count(‘алиса’)
words = contents.split()
n_words = len(words)
print(f»Книга ‘Алиса в стране чудес’ содержит {n_words} слов.»)
print(f»Имя Алиса повторяется {n_alice} раз.»)
При открытии файла добавился аргумент encoding=’utf-8′. Он необходим, когда кодировка вашей системы не совпадает с кодировкой читаемого файла. После чтения файла, сохраним его в переменной contents.
Для подсчета вхождения слова или выражений в строке можно воспользоваться методом count(), но прежде привести все слова к нижнему регистру функцией lower(). Количество вхождений сохраним в переменной n_alice.
Чтобы подсчитать количество слов в тексе, воспользуемся методом split(), предназначенный для построения списка слов на основе строки. Метод split() разделяет строку на части, где обнаружит пробел и сохраняет все части строки в элементах списка. Пример метода split():
title = ‘Алиса в стране чудес’
print(title.split())
[‘Алиса’, ‘в’, ‘стране’, ‘чудес’]
После использования метода split(), сохраним список в переменной words и далее подсчитаем количество слов в списке, с помощью функции len(). После подсчета всех данных, выведем на экран результат:
Книга ‘Алиса в стране чудес’ содержит 28389 слов.
Имя Алиса повторяется 419 раз.
2.1. Запись в пустой файл в Python
Самый простой способ сохранения данных, это записать их в файл. Чтобы записать текс в файл, требуется вызвать open() со вторым аргументом, который сообщит Python что требуется записать файл. Пример программы записи простого сообщения в файл на Python:
filename = ‘memory.txt’
with open(filename, ‘w’) as file:
file.write(«Язык программирования Python»)
Для начала определим название и тип будущего файла и сохраним в переменную filename. Затем при вызове функции open() передадим два аргумента. Первый аргумент содержит имя открываемого файла. Второй аргумент ‘ w ‘ сообщает Python, что файл должен быть открыт в режиме записи. Во второй строчке метод write() используется для записи строки в файл. Открыв файл ‘ memory.txt ‘ вы увидите в нем строку:
Язык программирования Python
Получившийся файл ничем не отличается от любых других текстовых файлах на компьютере, с ним можно делать все что угодно.
Важно: Открывая файл в режиме записи ‘ w ‘, если файл уже существует, то Python уничтожит его данные перед возвращением объекта файла.
Файлы можно открывать в режимах:
- чтение ‘ r ‘
- запись ‘ w ‘
- присоединение ‘ a ‘
- режим как чтения, так и записи ‘ r+ ‘
2.2. Многострочная запись в файл на Python
При использовании функции write() символы новой строки не добавляются в записываемый файл:
filename = ‘memory.txt’
with open(filename, ‘w’) as file:
file.write(«Язык программирования Python»)
file.write(«Язык программирования Java»)
file.write(«Язык программирования Perl»)
В результате открыв файл мы увидим что все строки склеились:
Язык программирования PythonЯзык программирования JavaЯзык программирования Perl
Для написания каждого сообщения с новой строки используйте символ новой строки n
filename = ‘memory.txt’
with open(filename, ‘w’) as file:
file.write(«Язык программирования Pythonn«)
file.write(«Язык программирования Javan«)
file.write(«Язык программирования Perln«)
Результат будет выглядеть так:
Язык программирования Python
Язык программирования Java
Язык программирования Perl
2.3. Присоединение данных к файлу на Python
Для добавления новых данных в файл, вместо того чтобы постоянно перезаписывать файл, откройте файл в режиме присоединения ‘ a ‘. Все новые строки добавятся в конец файла. Возьмем созданный файл из раздела 2.2 ‘memory.txt’. Добавим в него еще пару строк.
filename = ‘memory.txt’
with open(filename, ‘a’) as file:
file.write(«Hello worldn»)
file.write(«Полет на лунуn»)
В результате к нашему файлу добавятся две строки:
Язык программирования Python
Язык программирования Java
Язык программирования Perl
Hello world
Полет на луну
3. Исключения в Python
При выполнении программ могут возникать ошибки, для управления ими Python использует специальные объекты, называемые исключениями. Когда в программу включен код обработки исключения, ваша программа продолжится, а если нет, то программа остановится и выведет трассировку с отчетом об исключении. Исключения обрабатываются в блоках try-except. С блоками try-except программы будут работать даже в том случае, если что-то пошло не так.
3.1. Блоки try-except на Python
Приведем пример простой ошибки деления на ноль:
print(7/0)
Traceback (most recent call last):
File «example.py», line 1, in <module>
print(7/0)
ZeroDivisionError: division by zero
Если в вашей программе возможно появление ошибки, то вы можете заранее написать блок try-except для обработки данного исключения. Приведем пример обработки ошибки ZeroDivisionError с помощью блока try-except:
try:
print(7/0)
except ZeroDivisionError:
print(«Деление на ноль запрещено»)
Команда print(7/0) помещена в блок try. Если код в блоке try выполняется успешно, то Python пропускает блок except. Если же код в блоке try создал ошибку, то Python ищет блок except и запускает код в этом блоке. В нашем случае в блоке except выводится сообщение «Деление на ноль запрещено». При выполнение этого кода пользователь увидит понятное сообщение:
Деление на ноль запрещено
Если за кодом try-except следует другой код, то Python продолжит выполнение программы.
3.2. Блок try-except-else на Python
Напишем простой калькулятор, который запрашивает данные у пользователя, а затем результат деления выводит на экран. Сразу заключим возможную ошибку деления на ноль ZeroDivisionError и добавим блок else при успешном выполнение блока try.
while True:
first_number = input(«Введите первое число: «)
if first_number == ‘q’:
break
second_number = input(«Введите второе число: «)
if second_number == ‘q’:
break
try:
a = int(first_number) / int(second_number)
except ZeroDivisionError:
print(«Деление на ноль запрещено»)
else:
print(f»Частное двух чисел равно {a}»)
Программа запрашивает у пользователя первое число (first_number), затем второе (second_number). Если пользователь не ввел » q « для завершения работы программа продолжается. В блок try помещаем код, в котором возможно появление ошибки. В случае отсутствия ошибки деления, выполняется код else и Python выводит результат на экран. В случае ошибки ZeroDivisionError выполняется блок except и выводится сообщение о запрете деления на ноль, а программа продолжит свое выполнение. Запустив код получим такие результаты:
Введите первое число: 30
Введите второе число: 5
Частное двух чисел равно 6.0
Введите первое число: 7
Введите второе число: 0
Деление на ноль запрещено
Введите первое число: q
В результате действие программы при появлении ошибки не прервалось.
3.3. Блок try-except с текстовыми файлами на Python
Одна из стандартных проблем при работе с файлами, это отсутствие необходимого файла, или файл находится в другом месте и Python не может его найти. Попробуем прочитать не существующий файл:
filename = ‘alice_2.txt’
with open(filename, encoding=’utf-8′) as file:
contents = file.read()
Так как такого файла не существует, Python выдает исключение:
Traceback (most recent call last):
File «example.py», line 3, in <module>
with open(filename, encoding=’utf-8′) as file:
FileNotFoundError: [Errno 2] No such file or directory: ‘alice_2.txt’
FileNotFoundError — это ошибка отсутствия запрашиваемого файла. С помощью блока try-except обработаем ее:
filename = ‘alice_2.txt’
try:
with open(filename, encoding=’utf-8′) as file:
contents = file.read()
except FileNotFoundError:
print(f»Запрашиваемый файл {filename } не найден»)
В результате при отсутствии файла мы получим:
Запрашиваемый файл alice_2.txt не найден
3.4. Ошибки без уведомления пользователя
В предыдущих примерах мы сообщали пользователю об ошибках. В Python есть возможность обработать ошибку и не сообщать пользователю о ней и продолжить выполнение программы дальше. Для этого блок try пишется, как и обычно, а в блоке except вы прописываете Python не предпринимать никаких действий с помощью команды pass. Приведем пример ошибки без уведомления:
ilename = ‘alice_2.txt’
try:
with open(filename, encoding=’utf-8′) as file:
contents = file.read()
except FileNotFoundError:
pass
В результате при запуске этой программы и отсутствия запрашиваемого файла ничего не произойдет.
Далее: Функции json. Сохранение данных Python
Назад: Классы в Python
Можно писать программы, которые обрабатывают выбранные исключения. Посмотрите на следующий пример, который запрашивает ввод у пользователя до тех пор, пока не будет введено правильное целое число, но позволяет пользователю прерывать программу, используя Ctrl-C или что-либо поддерживаемое операционной системой. Обратите внимание, что сгенерированное пользователем прерывание сигнализируется возбуждением исключения KeyboardInterrupt.
while True: try: x = int(input("Пожалуйста, введите целое число: ")) break except ValueError: print("Это не целое число. Попробуйте снова...")
try/except работает следующим образом
:
- Сначала выполняется инструкция
try— код между ключевыми словамиtryиexcept. - Если исключение не возникает, инструкция
exceptпропускается и выполнение оператораtryзавершается. - Если во время выполнения кода в инструкции
tryвозникает исключение, остальная часть кода этого блока пропускается. Затем, если тип исключения соответствует исключению, записанному после ключевого словаexcept, блок кода этой инструкцииexceptвыполняется, а затем выполнение программы продолжается после всей конструкцииtry. - Если возникает исключение, которое не соответствует исключению, записанному в инструкции
except, оно передается внешним операторамtry. Если обработчик не найден, то это считается необработанным исключением, следовательно выполнение останавливается с сообщением об ошибке выполнения.
Оператор try может содержать несколько инструкций except, чтобы указать обработчики для различных исключений. В этом случае будет выполнен только один обработчик. Обработчики обрабатывают исключения, возникающие только в соответствующей конструкции try, а не в других обработчиках, например вложенных, того же оператора try. Инструкция except может иметь несколько исключений в виде кортежа, заключенного в скобки, например:
except (RuntimeError, TypeError, NameError): pass
Класс в инструкции except совместим с исключением, если это тот же самый класс или его базовый класс, но не наоборот. Инструкция except, перечисляющая производный класс, не совместима с базовым классом. Например, следующий код будет печатать B, C, D в таком порядке:
class B(Exception): pass class C(B): pass class D(C): pass for cls in [B, C, D]: try: raise cls() except D: print('D') except C: print('C') except B: print('B')
Обратите внимание, что если бы инструкции except были отменены (с первым исключением B), то вывелось бы B, B, B — срабатывает первое совпадающее предложение except.
Последняя инструкция except может опустить имя исключения. Используйте это с крайней осторожностью, таким образом можно легко замаскировать реальную ошибку программирования! Здесь можно использовать sys.exc_info() или модуль traceback для вывода сообщения об ошибке или ее сохранения для дельнейшего анализа, а затем повторно вызвать исключение при помощи оператора raise, что позволяет вызывающей стороне также обработать исключение:
import traceback try: f = open('myfile.txt') s = f.readline() i = int(s.strip()) except OSError as err: print("OS error: {0}".format(err)) except ValueError: print("Не удалось преобразовать данные в целое число.") except: print("Непредвиденная ошибка...") # сохраняем исключение для дальнейшего анализа. with open('trace.txt', 'a') as fp: traceback.print_exc(file=fp) # повторный вызов исключения, если это необходимо. raise
В примере выше, сведения об ошибке сохраняются в файл trace.txt с использованием встроенной функции open(). Лучшей практикой сохранения исключений для дальнейшего анализа в подобных ситуациях является применение модуля logging.
Конструкция try/except может содержать необязательную инструкцию else, которая при наличии должна следовать за всеми инструкциями except. Блок кода в инструкции else будет выполнен в том случае, если код в инструкции try не вызывает исключения. Например:
for arg in sys.argv[1:]: try: f = open(arg, 'r') except OSError: print('не удается открыть', arg) else: print(arg, 'имеет', len(f.readlines()), 'строк') f.close()
Использование инструкции else лучше, чем добавление дополнительного кода в инструкцию try, поскольку позволяет избежать случайного перехвата исключения, которое не было вызвано кодом, защищенным конструкцией try/except.
Когда возникает исключение, оно может иметь связанное значение, также известное как аргумент исключения. Наличие и тип аргумента зависят от типа исключения.
Инструкция except может указывать переменную после имени исключения. Переменная привязывается к экземпляру исключения с аргументами, хранящимися в экземпляре instance.args. Для удобства, экземпляр исключения определяет __str__(), так что аргументы могут быть напечатаны непосредственно без необходимости ссылаться instance.args. Кроме того, можно создать экземпляр исключения прежде, чем вызвать его и добавить любые атрибуты к нему по желанию.
try: raise Exception('spam', 'eggs') except Exception as inst: # экземпляр исключения print(type(inst)) # аргументы, хранящиеся внутри print(inst.args) # __str__ позволяет печатать args напрямую, но может # быть переопределен в подклассах исключений print(inst) # распаковка аргументов x, y = inst.args print('x =', x) print('y =', y) # <class 'Exception'> # ('spam', 'eggs') # ('spam', 'eggs') # x = spam # y = eggs
Если исключение имеет аргументы, они печатаются как последняя часть («деталь») сообщения для необработанных исключений.
Обработчики исключений обрабатывают не только исключения возникающие непосредственно в предложении try, но также если они возникают внутри функций, которые вызываются, даже косвенно, в коде оператора try. Например:
def this_fails(): x = 1/0 try: this_fails() except ZeroDivisionError as err: print('Handling run-time error:', err) #Handling run-time error: division by zero
Обработка ошибок увеличивает отказоустойчивость кода, защищая его от потенциальных сбоев, которые могут привести к преждевременному завершению работы.

Прежде чем переходить к обсуждению того, почему обработка исключений так важна, и рассматривать встроенные в 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 их обрабатывает.
Очень важно поупражняться в их использовании, чтобы сделать свой код более отказоустойчивым.
Перевод статьи Learning (not) to Handle Exceptions
В этой статье мы изучим основы обработки ошибок выполнения вашего кода в Python, а также научимся использовать свои собственные (пользовательские) исключения. Вы узнаете, почему иногда лучше не перехватывать исключения и как разработать свой шаблон написания кода, с которым будет удобно работать другим пользователям. Исключения являются важной частью любой программы Python, и их элегантное и эффективное применение на практике значительно повысит ценность и полезность вашего кода.
Примечание переводчика. Многие примеры кода, которые используется в этой статье имеют цель продемонстрировать особенности использования механизма исключений в языке Python. Иногда для наглядности представления особенностей использования инструкций кода, в ущерб практической направленности, некоторые примеры кода довольно надуманы.
Как всегда, вы можете самостоятельно ознакомиться с примерами кода для этой статьи, а также её исходным текстом, и если у вас есть предложения по его улучшению, пишите.
Содержание
- Инструкции Try/Except
- Перехват исключений заданного типа
- Повторное возбуждение исключений
- Использование исключений в исключениях
- Обработка нескольких исключений
- Блок finally
- Блок else
- Работа с traceback (трассировка)
- Применение пользовательских исключений
- Примеры хороших практик использования пользовательских исключений
- Добавляем к исключениям аргументы
- Выводы
Инструкции Try/Except
Допустим, что вы хотите прочитать из файла некоторые данные. Для решения этой задачи вы можете написать что-то вроде этого:
f = open('my_file.dat')
data = f.readfile()
print('Data loaded')
И если вы выполните этот код и файла с именем my_file.dat в рабочей директории нет, то увидите следующее сообщение:
FileNotFoundError: [Errno 2] No such file or directory: 'my_file.dat'
То есть такого файла или директории нет и после появления ошибки код вашей программы не будет выполнен полностью — произойдет её аварийное завершение. Вот почему вы не увидите результата выполнения последней инструкции: сообщения Data loaded в консоли. Это простой пример, часто возникающих на практике ошибок.
Теперь, представьте, что ваша программа в ходе выполнения обращается к некоторому устройству. И если в вашей программе возникает ошибка, то у вас не будет возможности корректно закрыть соединение, чтобы надлежащим образом освободить ресурсы с которыми работало ваше приложение. Это может привести как к потере данных, так и выводу из строя оконечного устройства, которым она управляла.
Работа с такого рода ошибками обычно выполняется с использованием блока инструкций try/except . Из официальной документации известно, что если внутри блока try в коде возникает ошибка, то будет выполнен код в блоке except. С учетом сказанного перепишем код из примера выше следующим образом:
try:
f = open('my_file.dat')
f.readfile()
print('Loaded data')
except:
print('Data not loaded')
Если теперь мы его запустим на выполнение, то увидим на экране сообщение: Data not loaded (Данные не загружены). И это замечательно! Теперь наша программа аварийно не завершает свою работу, и мы можем корректно закрыть соединение или прервать связь с нашим устройством. Однако мы всё ещё не знаем причину, по которой данные не были загружены.
Прежде чем продолжить, создадим пустой файл с именем my_file.dat в рабочей директории и снова запустим наш скрипт на исполнение. И увидим, что данные всё же не загружаются, независимо от того, существует файл или нет, так как наш файл пуст. Таким образом при разработке вашего проекта множество неопределенностей и неоднозначных ситуаций, которые возникают в коде блока except даже в таком простом примере. То есть мы не знаем почему данные не загружены: либо файла нет, либо же он просто пуст, ничего не содержит. Давайте рассмотрим следующий пример кода:
open('my_file.dat')
f.readfile()
print('Loaded data')
После его выполнения получим в консоли ошибку вида:
AttributeError: '_io.TextIOWrapper' object has no attribute 'readfile'
Сообщение об ошибке информирует нас о том, что проблема заключается в том методе readfile, который мы попытались использовать, и что его не существует в полученном объекте.
Если вы используете простой блок try/except , вы будете обрабатывать абсолютно все возможные исключения, но на самом деле у нас нет возможности узнать, что всё же пошло не так, то есть причину появления ошибки. В простых случаях, таких как приведен выше, у вас есть только две строчки кода, которые можно легко проанализировать и найти причину ошибки. Однако, если вы разрабатываете свой пакет или сложную функцию с большим числом вложений кода, то ошибки будут распространяться далее по ходу потока исполнения кода, и вы не знаете, как они повлияют на работу остальной части программы и что именно вызвало их появление.
Чтобы вы имели представление о важности правильной обработки ошибок, я приведу вам пример, ситуации, возникшей при эксплуатации прикладного приложения, которое поставлялось вместе со сложным лабораторным оборудованием. Эта программа управляет прибором Nano Sight и имеет очень приятный пользовательский интерфейс. Однако при сохранении данных, если выбранное имя файла содержало точку, данные не сохранялись. К сожалению, в этом случае данные безвозвратно терялись, и пользователь никогда не узнал бы, что причина возникновения проблемы на самом деле проста: наличие символа . в имени файла.
Поэтому разработка изящного способа обработки всех возможных ошибок очень сложна, а иногда практически невозможна. Тем не менее, вы можете видеть, что программное обеспечение, тем более поставляемое с очень дорогим оборудованием (в данном случае я имею в виду инструменты с ценой, как у небольшой квартиры), должно учитывать возникновение всевозможных видов ошибок и корректную обработку возбужденных исключений.
Перехват исключений заданного типа
Самый правильный способ обработки исключений в Python — указать, какой тип исключения мы ожидаем и соответствующим образом его обработать . Таким образом, если мы узнаём, что проблема в том, что файла не существует, то мы можем его создать. Если же проблема имеет другую природу, то будет генерироваться сообщение с информацией об исключении и отображаться для информирования пользователя. Давайте изменим пример, приведенный выше, следующим образом:
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except FileNotFoundError:
print('This file doesn't exist')
Если вы запустите этот скрипт, а файла my_file.dat в рабочей директории нет, то он выведет на экран сообщение, что файл не существует, и программа продолжит работу. Однако, если файл существует, вы увидите исключение связанное с отсутствием у объекта file метода readfile (такого метода конечно же не существует — это преднамеренная ошибка).
На самом деле мы можем ограничиваться простой печатью сообщения, если происходит перехват исключения — в случае отсутствия файла его легко создать:
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except FileNotFoundError:
file = open('my_file.dat', 'w')
print('File created')
file.close()
Если вы запустите этот скрипт, то увидите, что теперь файл my_file.dat будет создан. Если вы запустите скрипт во второй раз, то будет возбуждено исключение из-за отсутствия метода readfile.
Теперь представьте, что вы не указываете, какое конкретно исключение вы собираетесь перехватывать, и у вас есть следующий код, что произойдет, когда вы его запустите?
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except:
file = open('my_file.dat', 'w')
print('File created')
Если вы его внимательно проанализируете, то поймете, что, даже если файла my_file.dat существует, будет возбуждено исключение, ввиду отсутствия метода readfile. Затем будет выполнен код в блоке except. В этом блоке наша программа создаст новый файл my_file.dat, даже если он уже существует, и, следовательно, вы потеряете все информацию, которая в нем до этого хранилась. Что же мы можем сделать для того, что бы избежать этого рассмотрим далее.
Повторное возбуждение исключений
На практике распространен следующий приём написания кода, когда сначала возбуждается некоторое исключение, далее вы выполняете определённые действия, а затем снова инициализируется это же исключение. Этот прием используется, например, если вы осуществляете запись информации в базу данных или в файл. Представим себе следующую ситуацию: вы храните информацию в двух файлах: в первом вы храните значения частотного спектра, а во втором — температуру, при этом записи в отдельных файлах записываются по порядку и их соответствующие значения снимаются с датчиков в один и тот же момент времени. Далее вы сначала сохраняете в один файл значения спектра, а затем значение температуры уже в другой файл. То есть каждая строка в любом из файлов соответствует в прямом порядке следования некоторой другой строке в другом.
Предположим, что сначала мы сохраняем значения спектра, а затем температуры. Однако время от времени, когда вы пытаетесь считывать показания датчиков у прибора, он сбоит, и значение датчика температуры не считывается. Таким образом, если в этот цикл записи мы не сохраним значение температуры, у вас образуется несоответствие в порядке данных по времени их съёма, потому что во втором файле будет отсутствует строка данных, соответствующая показаниям датчика температуры. В то же время вам не нужно, чтобы эксперимент продолжался далее, так как датчик температуры не исправен и вам необходимо провести с ним некоторые манипуляции. Для решения этой задачи вы можете использовать такой пример кода:
#Данные сохранены. Начало нового цикла опроса датчиков
try:
temp = instrument.readtemp() # пытаемся опросить датчик температуры
except:
remove_last_line(data_file)
# если датчик не отвечает то удалим последнюю запись в файле, записанную в текущем цикле
raise
# возбудим это исключение снова для того, чтобы пользователь был оповещен о возникшей ошибке
save_temperature(temp)
# если все нормально сохраняем показания датчика в файл
Таким образом сначала мы пытаемся получить данные от датчика температуры, и если что-то пойдет не так, мы перехватываем возбужденное исключение, инициированное из-за того, что датчик нам не отвечает. Затем мы удаляем последнюю строку из нашего первого файла с данными о спектре, а затем снова инициируем исключение с помощью оператора raise. Эта команда повторно возбуждает исключение того же типа, что было перехвачено в блоке except. Благодаря этой стратегии мы будем уверены в том, что наши данные в обоих файлах будут согласованны (то есть имеют одинаковую длину или количество записей). А также в том, что программа далее не будет продолжать работать в том, же режиме (и опрашивать нерабочие датчики), и пользователь увидит сообщение с всей необходимой информации о том, что же пошло не так (тип ошибки).
Существует еще один способ практического применения повторного возбуждения исключений. И если кто-то, из читающих эту статью, знает другие способы использования этого приема, напишите мне и я обновлю содержимое статьи с учетом ваших дополнений.
Дополнение. Дело в том, что инструкция except, из примера кода выше, осуществляет перехват абсолютно всех типов исключений, возникающих в ходе выполнения программы. Если же справа от нее указать название класса перехватываемого исключения (или кортеж в котором они перечислены), то этот блок кода будет выполнен лишь при перехвате исключения соответствующего класса.
Рассмотрим следующий пример кода.
try:
# код, который мы проверяем
except:
# при перехвате любого типа исключений
# этот блок кода будет выполнен
raise
except Exception_classname1:
# при перехвате исключения с классом Exception_name1
# этот блок кода будет выполнен
except Exception_classname2:
# при перехвате исключения с классом Exception_name2
# этот блок кода будет выполнен
Как видно первый блок except будет перехватывать абсолютно все типы исключений и соответственно при возникновении любой исключительной ситуации код внутри этого блока будет выполнен всегда. Следующие блоки except будут выполняться только при перехвате исключений соответствующих классов Exception_classname1 и Exception_classname2 .
А значит в первый блок except можно поместить код, который необходимо выполнять при возникновении исключительной ситуации любого вида. После чего инициируем возбуждение исключения того же типа с помощью инструкции raise.
Следующие блоки except с соответствующими классами будут перехватывать заданные типы исключений и внутри их блоков можно добавить код, который будет выполняться в зависимости от вида возникшей исключительной ситуации. Не правда ли, что это очень удобно.
С синтаксисом использования инструкции except вы можете ознакомиться по ссылке. С классами исключительных ситуаций по этой ссылке.
Использование исключений в исключениях
Теперь представим, что наш код является частью функции, отвечающей за открытие файла, загрузку его содержимого или создание нового файла в случае, если файла с указанным именем не существует. Сценарий будет выглядеть так же, как и ранее, с той разницей, что имя файла будет задаваться в переменной:
try:
file = open(filename)
data = file.readfile()
except FileNotFoundError:
file = open(filename, 'w')
Чтобы запустить приведенный этот скрипт на выполнение, мы должны указать имя файла:
filename = 'my_data.dat'
try :
[ ... ]
Если вы запустите этот код, то заметите, что он ведет себя точно так, как ожидалось. Однако, если вы укажете пустое имя файла:
filename = ''
try:
[...]
Вы увидите выведенное на экран сообщение с большим количеством информации о возникшей ошибке, содержащей лишь одну важную строку:
During handling of the above exception, another exception occurred:
Если вы её проанализируете, то увидите, что во время обработки вышеупомянутого исключения было возбуждено другое исключение. К сожалению, это обычная ситуация, особенно когда речь идет о пользовательском вводе.
Решить возникшую причину можно, вложив в наш блок except еще один блок инструкций try/except или же предварительно проверять валидность значения переменной filename перед вызовом инструкции open. Но самый правильный способ избежать возникновения этой ситуации мы рассмотрим ниже.
Обработка нескольких исключений
До сих пор мы рассматривали случаи возбуждения только одного типа исключений, например FileNotFoundError. Тем не менее, листинг кода, который мы уже рассматривали выше, вызывает появление двух исключений различных типов, а точнее FileNotFoundError и AttributeError. Рассмотрим приемы работы с несколькими типами исключений на практике.
Конечно же вы можете специально создавать ситуации, приводящие к возбуждению исключений. Например, если вы запустите на выполнение следующий код:
file = open('my_data.dat', 'a')
file.readfile()
То получите сообщение:
AttributeError: '_io.TextIOWrapper' object has no attribute 'readfile'
Первая часть полученного сообщения (а точнее до символа :) — это имя класса возбужденного исключения, то есть AttributeError, а вот вторая часть — это информация о причине, вызвавшей инициализацию полученного исключения. Отметим, что исключения одного типа могут иметь разное содержимое второй части, сообщения об ошибке, а значит и причины инициализации исключения, а следовательно подробнее описывают то, что его вызвало.
Для решения нашей задачи необходимо перехватить исключения 2 типов: AttributeError и FileNotFound. Поэтому наш код будет иметь вид:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.readfile()
except FileNotFoundError:
file = open(filename, 'w')
print('Created file')
except AttributeError:
print('Attribute Error')
Теперь мы знаем как можно обрабатывать в программе несколько типов исключений. Отметим, что если в какой-либо строке кода внутри блока try возникает исключение, то остальная часть кода в этом блоке не будет выполнена. То есть если файла my_data.dat не существует то, следующая строка кода, то есть data = file.readfile() не будет выполняться.
Далее поток выполнения интерпретатора Python будет последовательно обходить все except. И если будет найдено соответствие типа возбужденного исключения с классом исключений справа от каждой инструкции except, то код внутри этого блока будет выполнен. Поэтому всегда единовременно будет возбуждено только одно исключение определенного типа.
В нашем же случае при первом запуске кода файла my_data.dat не существует, то есть будет инициализироваться исключение типа — FileNotFoundError. Файл будет создан и в консоли мы получим сообщение Created file. При повторном запуске нашего кода будет возбуждено исключение класса AttributeError. Так файл существует, но созданный объект file не имеет метода readfile(), то попытка его вызвать завершится возбуждением соответствующего исключения.
Отметим, что мы можем добавить в конце нашего кода еще один блок except без наименования класса исключения справа для того, чтобы перехватывать все другие исключения, возникающие в ходе выполнения нашей программы:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except FileNotFoundError:
file = open(filename, 'w')
print('Created file')
except AttributeError:
print('Attribute Error')
except:
print('Unhandled exception')
В случае если файл my_data.dat существует, но он пуст, у нас возникнет еще одна проблема при попытке доступа к содержимому data[0] (получить первый символ строки, в которую мы прочитали содержимое файла). Мы не готовы к обработке этого исключения и поэтому напечатаем лишь сообщение о необработанном исключении (Unhandled exception). Тем не менее, было бы более полезно сообщать пользователю, какое же исключение было возбуждено. Для этого мы можем использовать следующий приём:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
print('Unhandled exception')
print(e)
Результатом выполнения этого кода будет следующее сообщение:
Unhandled exception string index out of range
Так как исключения имеют свой определённый тип type, соответствующий наименованию предопределенных классов исключений (официальная документация), то его также можно использовать. Например, следующим образом:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
print('Unhandled exception')
if isinstance(e, IndexError):
print(e)
data = 'Information'
important_data = data[0]
print(important_data)
После запуска этого кода на выполнение, он напечатает в консоли первую букву строки Information, то есть I.
Отметим, что изучение этого примера кода имеет своей целью продемонстрировать возможности использования информации о возбужденном исключении, в ущерб практической значимости. Как мы можем видеть, его главный недостаток заключается в том, что переменная important_data будет в конечном итоге не определена. Это произойдет в случае если файл my_data.dat не существует. И поэтому мы получим соответствующее сообщение об ошибке:
NameError: name 'important_data' is not defined
Чтобы не допустить таких ситуаций мы можем добавить еще один блок в последовательность блоков-перехватчиков except, это блок с инструкцией finally.
Блок finally
Код в блоке с инструкцией finally будет выполняться всегда независимо от того, возбуждались ли исключения в блоке try или нет.
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
if isinstance(e, IndexError):
print(e)
data = 'Information'
important_data = data[0]
else:
print('Unhandled exception')
finally:
important_data = 'A'
print(important_data)
Приведенный выше пример кода очень прост, но совершенно не применим на практике, так как мы специально устанавливаем значение переменной important_data. Тем не менее он наглядно поясняет принцип использования оператора finally . Таким образом если необходимо в коде производить какие либо действия с данными или внешними устройствами, и вы хотите быть абсолютно уверены, что они будут выполнены в любом случае (при возникновении любых ошибок или сбоев), то вы можете включить код этих операций в блок инструкции finally.
Используя блок finally можно быть уверенным, что вы закроете соединение с устройством или файл, открытый для записи или чтения и т. д. Или в общем говоря, корректно освободите все ресурсы, задействованные при работе с вашим программным обеспечением.
И наконец, поведение блока finally, в частности и в нашем примере, будет разным, в зависимости от того сколько раз мы его запустим на исполнение. Давайте рассмотрим следующий пример:
filename = 'my_data.dat'
try:
print('In the try block')
file = open(filename)
data = file.read()
important_data = data[0]
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
finally:
print('Finally, closing the file')
file.close()
important_data = 'A'
print(important_data)
Сначала запустим код, для ситуации когда файла my_data.dat не существует. И увидим следующий результат его выполнения:
In the try block File not found, creating one Finally, closing the file
Итак, мы видим , что код последовательно выполнялся в блоках: сначала в try, затем во всех блоках except и наконец в finally. Если вы снова запустите код, то файл уже будет существовать, и, следовательно, результат его выполнения будет совершенно другим:
In the try block
Finally, closing the file
Traceback (most recent call last):
File "JJ_exceptions.py", line 7, in
important_data = data[0]
IndexError: string index out of range
Из полученного сообщения мы видим, что когда возникает необработанное исключение, следующий блок, который будет выполнен, — это finally . Поэтому у нас появляется возможность корректно закрыть наш файл до аварийного завершения программы (освободить ресурсы).
Этот подход удобен тем, что предотвращает любой конфликт с кодом ниже. Вы открываете, а затем при возникновении ошибки закрываете файл, при этом остальная часть нашей программы решает проблему обработки исключения типа IndexError. Если вы хотите запустить наш пример без инициализации исключений, просто напишите что либо в файл my_data.dat, открыв его в простом текстовом редакторе, и вы увидите результат выполнения нашей программы без ошибок и соответственно возбуждения исключений.
Блок else
Изучая вопросы обработки и инициализации исключений, необходимо рассмотреть еще одну инструкцию, которую можно использовать совместно с уже рассмотренными — это инструкция else . Основная идея её использования заключается в том, что код внутри её блока выполняется, если в блоке try не возникло ошибок, значит не инициировалось возбуждение исключений . Из следующего примера, можно очень легко понять как это работает:
filename = 'my_data.dat'
try:
file = open(filename)
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
data = file.read()
important_data = data[0]
Самое сложное — это понять полезность использования блока else . И в принципе, код, который мы включили в блок else, мог быть помещен сразу после строки кода, в которой мы открываем файл, как мы это делали ранее. Тем не менее, мы можем использовать блок else для предотвращения перехвата исключений, которые не относятся к коду в блоке try .
Не понятно? Тогда рассмотрим следующий немножко надуманный, но наглядный пример кода. Представьте, что вам необходимо прочитать имя файла из другого файла, а затем открыть его для чтения/записи. Наш код для решения этой задачи будет выглядеть следующим образом:
try:
file = open(filename)
new_filename = file.readline()
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
new_file = open(new_filename)
data = new_file.read()
Поскольку мы открываем по очереди два файла, то вполне возможно, что проблема будет в том, что второго файла с указанным именем не существует. Если бы мы поместили этот код в блок try, то мы в конечном итоге в блоке except перехватим исключение, инициированное из-за ошибки открытия второго файла, даже если нам нужно перехватить ошибку открытия первого. Конечно этот пример может быть не совсем правдоподобен, но он наглядно поясняет суть использования инструкции else.
Теперь объединим всё, что мы узнали о приемах обработки и инициализации исключений вместе:
try:
file = open(filename)
new_filename = file.readline()
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
new_file = open(new_filename)
data = new_file.read()
finally:
file.close()
Проаназируйте его самостоятельно. Настоятельно рекомендую позже поиграть с примерами кода, представленными в этой статье и посмотреть на практике работу рассмотренных приёмов использования каждого блока инструкций. Если вы еще не достаточно хорошо знаете Python, то возможно, вы столкнетесь с чем то для вас непонятным, и это будет связано с инициализацией и обработкой исключений. Это заставит вас заново запускать скрипты и анализировать полученные результаты снова и снова.
Теперь вы знаете, что использование механизма инициализации и обработки исключений, может применяться на практике разными способами. Отличным ресурсом, содержащим все те материалы, что мы с вами рассмотрели в этой статье, является, конечно же, официальная документация использования исключений в Python.
Работа с traceback (трассировка)
Как вы, наверное, уже могли видеть, когда инициализируется исключение, то на экран консоли выводится большое количество различной информации, описывающей что же все-таки произошло. Например, если вы попытаетесь открыть несуществующий файл, то вы получите следующее сообщение:
Traceback (most recent call last):
File "P_traceback.py", line 13, in
file = open(filename)
FileNotFoundError: [Errno 2] No such file or directory: 'my_data.dat'
Интерпретация получаемых после возбуждения исключений сообщений требует наличия некоторой практики, но для простых случаев это достаточно легко сделать даже начинающим.
Во-первых, в сообщении приведен traceback, или простыми словами, история всех действий интерпретатора, результат выполнения которых привёл к инициализации исключения. И конечно сам процесс детальной расшифровка traceback требует написания отдельного поста.
Во-вторых из нашего примера сообщения сразу можно определить путь к файлу, который стал причиной возникшей ошибки, а также номер строки исходного кода скрипта, выполнение которой привело к ошибке. Если затем вы откроете файл исходного кода в текстовом редакторе и перейдете к этой строке, то увидите, что это строка, содержащую следующие инструкции: file = open(filename). И наконец из сообщения, вы можете узнать тип возникшего исключения FileNotFoundError, также как мы это делали ранее.
В последнем примере с выведенным на экран сообщением о возбужденном исключения мы пренебрегали использованием модуля traceback из состава стандартной библиотеки Python. Что позволило бы нам более точно определить источник ошибок, приведший к возбуждению исключений. К счастью, Python и возможности его стандартной библиотеки позволяют легко получить доступ к информации traceback и осуществлять трассировку выполнения скрипта в ходе отладки. Немного изменив наш пример с открытием файла, получим:
import traceback
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
except FileNotFoundError:
traceback.print_exc()
Если вы снова запустите код, то увидите на экране ту же информацию как и в примере сообщения об исключении выше, но при этом импортируемый модуль traceback предоставляет больше возможностей для трассировки вашей программы. Работа с пошаговыми трассировками выполнения вашего кода очень удобна для отладки.
Примеры, которые мы рассмотрели в этой статье, очень просты, но когда у вас проект с очень сложным кодом, с большим числом вложений, например, когда одна функция вызывает другую, которая в свою очередь создает объект, и запускает его метод и т. д. В этом случае узнать, что же вызвало возбуждение исключения и в каком месте вашего кода, становится сложной нетривиальной задачей.
Применение пользовательских исключений
Если вы будете заниматься разработкой пакетов, часто бывает полезно определить собственные типы исключений. Это дает большую гибкость вашим приложениям, поскольку позволяет другим разработчикам корректно обрабатывать их по своему усмотрению. И так давайте рассмотрим эту идею на следующем примере.
Представьте, что вы разрабатываете код, содержащий функцию, которая вычисляет среднее двух чисел, но вы хотите, чтобы оба эти числа были положительными. Это тот же пример иллюстрирует паттерн проектирования, известный как декоратор.
Начнем с определения нашей функции:
def average(x, y):
return (x + y)/2
Далее мы хотим возбуждать исключение Exception, в случае если любой из входных аргументов функции отрицателен. Для этого сделаем следующее:
def average(x, y):
if x<=0 or y<=0:
raise Exception('Both x and y should be positive')
return (x + y)/2
Если вы попробуете это вызвать нашу функцию с отрицательным параметром, в консоли вы увидите следующее:
Exception: Both x and y should be positive
Как видно, мы получили то, что хотели. Однако, если вы создаете пакет и ожидаете, что другие разработчики будут его использовать, было бы лучше определить пользовательское исключение, которое затем можно будет явно перехватить. С учётом сказанного наш код может выглядеть следующим образом:
class NonPositiveError(Exception):
pass
def average(x, y):
if x <= 0 or y <= 0:
raise NonPositiveError('Both x and y should be positive')
return (x + y) / 2
Объявляем класс нашего исключения, который будет наследовать от общего класса Exception. На данном этапе, мы просто будем выполнять инструкцию pass в теле нашего класса исключения.
Если мы запустим приведенный выше код с отрицательным значением входного параметра функции, то получим:
NonPositiveError: Both x and y should be positive
Теперь если вы захотите далее в коде перехватывать ваше пользовательское исключение типа NonPositiveError, то будете делать так как мы делали это ранее. Единственным отличием будет то, что пользовательские исключения по умолчанию недоступны, и их следует импортировать. Например, следующим образом:
from exceptions import NonPositiveError
from tools import average
try:
avg = average(1, -2)
except NonPositiveError:
avg = 0
Если вы достаточно долго работали с пакетами, то возможно уже сталкивались с большим разнообразием видов пользовательских исключений, подобных уже нами рассмотренным. Они являются отличным инструментом, позволяющим пользователю точно указать, что случилось при выполнении кода пакета, и далее действовать соответствующим образом. А значит мы должны быть готовы к обработке исключений различных типов: как пользовательских, так и общих, определенных в Python. Отметим, что в сообществе разработчиков Python приветствуется использование в своих пакетах и модулях пользовательских исключений, что облегчает их поддержку и использование.
Примеры хороших практик использования пользовательских исключений
Как мы уже говорили выше, при разработке пакета, очень удобно определять собственные пользовательские исключения, которые относятся только к выполнению кода пакета. Это значительно облегчает в дальнейшем разработку вашего приложения и дает другим разработчикам эффективный инструмент для определения проблем при выполнении кода в вашем пакете. Представьте, например, что вы работаете с приложением, использующим в качестве зависимости ваш пакет и хотите записывать какую-либо информацию в файл каждый раз, когда возбуждается исключение в коде вашего пакета.
Это достаточно легко, так из официальной документации известно, что типы всех исключений могут наследоваться от одного базового класса. Приведенный ниже пример кода достаточно большой, но включает в себя код всех примеров, рассмотренных нами выше, и поэтому его анализ будет несложен:
class MyException(BaseException):
pass
class NonPositiveIntegerError(MyException):
pass
class TooBigIntegerError(MyException):
pass
def average(x, y):
if x<=0 or y<=0: raise NonPositiveIntegerError('Either x or y is not positive') if x>10 or y>10:
raise TooBigIntegerError('Either x or y is too large')
return (x+y)/2
try:
average(1, -1)
except MyException as e:
print(e)
try:
average(11, 1)
except MyException as e:
print(e)
try:
average('a', 'b')
except MyException as e:
print(e)
print('Done')
Сначала определим свой класс исключений MyException, которой станет нашим базовым классом. Затем определим два класса исключений NonPositiveIntegerError и TooBigIntegerError, которые наследуют от MyException . Далее определим функцию average, но на этот раз в ней будет инициализироваться два разных типа исключений соответствующих случаям: если одно или оба из чисел, передаваемых в качестве аргумента, отрицательны или больше 10.
Далее в нашем примере представлены варианты вызова функции average с различными значениями аргументов. Отметим, что в блоке try/except мы всегда будем перехватывать исключение типа MyException, соответствующего нашему базовому классу, а не одно из конкретных типов исключений.
В первых двух примерах, передавая числа -1 и 11 в качестве аргументов, мы успешно выводим на экран сообщение об ошибке, и программа продолжает работать. Однако, если мы попытаемся вычислить среднее значение между двумя буквами (строками), возбужденное исключение будет иметь другую причину инициализации и не будет перехвачено блоком Exception. Поэтому программа аварийно завершит работу и вы увидите на экране следующее сообщение:
TypeError: '<=' not supported between instances of 'str' and 'int'
Таким образом, основным преимуществом использования классов для обработки пользовательских исключений заключается в возможности указания базового класса для перехвата всех исключений соответствующих классов-потомков.
Добавляем к исключениям аргументы
Иногда удобно использовать синтаксис аргументов в дополнении к инструкциям исключений, для предоставления пользователям более информативного форматированного вывода сообщений об ошибках. На примере нашей функции для вычисления среднего определим класс исключения NonPositiveIntegerError c использованием аргументов:
class MyException(BaseException):
pass
class NonPositiveIntegerError(MyException):
def __init__(self, x, y):
super(NonPositiveIntegerError, self).__init__()
if x<=0 and y<=0:
self.msg = 'Both x and y are negative: x={}, y={}'.format(x, y)
elif x<=0:
self.msg = 'Only x is negative: x={}'.format(x)
elif y<=0:
self.msg = 'Only y is negative: y={}'.format(y)
def __str__(self):
return self.msg
def average(x, y):
if x<=0 or y<=0:
raise NonPositiveIntegerError(x, y)
return (x+y)/2
try:
average(1, -1)
except MyException as e:
print(e)
Как видно из этого примера, инструкция возбуждения исключения принимает два аргумента, x и y, и генерирует на их основе форматированное сообщение. Отметим, что в нашем примере сообщение об ошибке может содержать информацию о том, что аргументы, переданные в функцию average, могут быть как оба отрицательны, так и о каждом непосредственно.
То есть использование аргументов при возбуждении и обработки исключений, предоставляет не только общую информацию об ошибке, но и более детальную о причине ее появления. И это очень удобно.
Ключевая часть в объявлении нашего пользовательского класса исключений находится в конце его объявления — это метод __str__. Этот метод отвечает непосредственно за то, что появляется на экране, когда вы используете инструкцию print(e) в блоке except . В нашем примере мы просто возвращаем сообщение, генерированное методом __init__, но многие разработчики предпочитают генерировать сообщение в методе __str__ на основе параметров, переданных в конструктор класса.
Выводы
Исключения в консоли — это то, что никто не хочет видеть в ходе разработки и отладки проекта, но на практике без них никак. Возможно, вы попытаетесь прочитать файл, который не существует, или пользователь, использующий вашу программу, ввёл в неё недопустимые значения, или же матрица данных, которые вы пытаетесь обработать, содержит число измерений, отличное от ожидаемых вами и т. д.
Обработка исключений является достаточной деликатной темой, поскольку она может привести к еще большему количеству проблем. Исключение — это сообщение вам о том, что с вашим кодом происходит что-то не так, и если вы не исправите ошибку должным образом, дальше станет еще хуже.
В тоже время обработка исключений может помочь вам избежать несоответствия входных данных ожидаемым, не корректно освобождать ресурсы, закрывать соединения со внешними устройствами, соединения по сети или корректно закрывать файлы с данными и т. д.
Использование инструкций try/except очень удобно если вы точно знаете, какие могут возбуждаться исключения и как их затем обработать. Поэтому при работе с ними необходимо предусмотреть все возможные ситуации и спланировать перехват возбужденных исключений таким образом, чтобы работа программы не завершилась аварийно и не произошло утечек ресурсов и потери данных.
Как и в любой другой теме, посвященной Python или другому языку программирования, лучший способ изучить его — внимательно посмотреть на код другого и проанализировать его. Не все пакеты определяют свои собственные исключения и не обрабатывают их одинаково. Если вы ищете вдохновение, вы можете посмотреть ошибки Pint , в его небольшом простом пакете, или исключения Django, как пример гораздо более сложного проекта.
Код, и текст статьи доступны по ссылкам и если у вас есть какие-либо комментарии или предложения по его улучшению, пишите.

Одним из недостатков гибких языков, таких как Python, является предположение, что если что-то работает, то скорее всего оно сделано правильно. Я хочу написать скромное руководство по эффективному использованию исключений в Python, правильной их обработке и логировании.
Эффективная обработка исключений
Введение
Давайте рассмотрим следующую систему. У нас есть микросервис, который отвечает за:
· Прослушивание событий о новом заказе;
· Получение заказа из базы данных;
· Проверку состояния принтера;
· Печать квитанции;
· Отправка квитанции в налоговую систему (IRS).

В любой момент может сломаться что угодно. У вас могут возникнуть проблемы с объектом заказа, в котором может не быть нужной информации, или в принтере может закончиться бумага, или же сервис налоговой не будет работать, и вы не сможете синхронизировать с ними квитанцию об оплате, а может быть ваша база данных окажется недоступна.
Ваша задача правильно и проактивно реагировать на любую ситуацию, чтобы избежать ошибок при обработке новых заказов.
И примерно вот такой код на этот случай пишут люди (он, конечно, работает, но плохо и неэффективно):
class OrderService:
def emit(self, order_id: str) -> dict:
try:
order_status = status_service.get_order_status(order_id)
except Exception as e:
logger.exception(
f"Order {order_id} was not found in db "
f"to emit. Error: {e}."
)
raise e
(
is_order_locked_in_emission,
seconds_in_emission,
) = status_service.is_order_locked_in_emission(order_id)
if is_order_locked_in_emission:
logger.info(
"Redoing emission because "
"it was locked in that state after a long time! "
f"Time spent in that state: {seconds_in_emission} seconds "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": order_status.value}
elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
logger.info(
"Aborting emission because it already happened! "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
return {"order_id": order_id, "order_status": order_status.value}
try:
receipt_note = receipt_service.create(order_id)
except Exception as e:
logger.exception(
"Error found during emission! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise e
try:
broker.emit_receipt_note(receipt_note)
except Exception as e:
logger.exception(
"Emission failed! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise e
order_status = status_service.get_order_status(order_id)
return {"order_id": order_id, "order_status": order_status.value}
Сначала я сосредоточусь на том, что OrderService слишком много знает, и все эти данные делают его чем-то вроде blob, а чуть позже расскажу о правильной обработке и правильном логировании исключений.
Почему этот сервис — blob?
Этот сервис знает слишком много. Кто-то может сказать, что он знает только то, что ему нужно (то есть все шаги, связанные с формированием чека), но на самом деле он знает куда больше.
Он сосредоточен на создании ошибок (например, база данных, печать, статус заказа), а не на том, что он делает (например, извлекает, проверяет статус, генерирует, отправляет) и на том, как следует реагировать в случае сбоев.
В этом смысле мне кажется, что клиент учит сервис тому, какие исключения он может выдать. Если мы решим переиспользовать его на любом другом этапе (например, клиент захочет получить еще одну печатную копию более старого чека по заказу), мы скопируем большую часть этого кода.
Несмотря на то, что сервис работает нормально, поддерживать его трудно, и неясно, как один шаг соотносится с другим из-за повторяющихся блоков except между шагами, которые отвлекают наше внимание на вопрос «как» вместо того, чтобы думать о «когда».
Первое улучшение: делайте исключения конкретными
Давайте сначала сделаем исключения более точными и конкретными. Преимущества не видны сразу, поэтому я не буду тратить слишком много времени на объяснение этого прямо сейчас. Однако обратите внимание на то, как изменяется код.
Я выделю только то, что мы поменяли:
try:
order_status = status_service.get_order_status(order_id)
except Exception as e:
logger.exception(...)
raise OrderNotFound(order_id) from e
...
try:
...
except Exception as e:
logger.exception(...)
raise ReceiptGenerationFailed(order_id) from e
try:
broker.emit_receipt_note(receipt_note)
except Exception as e:
logger.exception(...)
raise ReceiptEmissionFailed(order_id) from e
Обратите внимание, что на этот раз я пользуюсь from e, что является правильным способом создания одного исключения из другого и сохраняет полную трассировку стека.
Второе улучшение: не лезьте не в свое дело
Теперь, когда у нас есть кастомные исключения, мы можем перейти к мысли «не учите классы тому, что может пойти не так» — они сами скажут, если это случится!
# Services
class StatusService:
def get_order_status(order_id):
try:
...
except Exception as e:
raise OrderNotFound(order_id) from e
class ReceiptService:
def create(order_id):
try:
...
except Exception as e:
raise ReceiptGenerationFailed(order_id) from e
class Broker:
def emit_receipt_note(receipt_note):
try:
...
except Exception as e:
raise ReceiptEmissionFailed(order_id) from e
# Main class
class OrderService:
def emit(self, order_id: str) -> dict:
try:
order_status = status_service.get_order_status(order_id)
(
is_order_locked_in_emission,
seconds_in_emission,
) = status_service.is_order_locked_in_emission(order_id)
if is_order_locked_in_emission:
logger.info(
"Redoing emission because "
"it was locked in that state after a long time! "
f"Time spent in that state: {seconds_in_emission} seconds "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": order_status.value}
elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
logger.info(
"Aborting emission because it already happened! "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
return {"order_id": order_id, "order_status": order_status.value}
receipt_note = receipt_service.create(order_id)
broker.emit_receipt_note(receipt_note)
order_status = status_service.get_order_status(order_id)
except OrderNotFound as e:
logger.exception(
f"Order {order_id} was not found in db "
f"to emit. Error: {e}."
)
raise
except ReceiptGenerationFailed as e:
logger.exception(
"Error found during emission! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise
except ReceiptEmissionFailed as e:
logger.exception(
"Emission failed! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Как вам? Намного лучше, правда? У нас есть один блок try, который построен достаточно логично, чтобы понять, что произойдет дальше. Вы сгруппировали конкретные блоки, за исключением тех, которые помогают вам понять «когда» и крайние случаи. И, наконец, у вас есть блок else, в котором описано, что произойдет, если все отработает как надо.
Кроме того, пожалуйста, обратите внимание на то, что я сохранил инструкции raise без объявления объекта исключения. Это не опечатка. На самом деле, это правильный способ повторного вызова исключения: простой и немногословный.
Но это еще не все. Логирование продолжает меня раздражать.
Третье улучшение: улучшение логирования
Этот шаг напоминает мне принцип «говори, а не спрашивай», хотя это все же не совсем он. Вместо того, чтобы запрашивать подробности исключения и на их основе выдавать полезные сообщения, исключения должны выдавать их сами – в конце концов, я их конкретизировал!
### Exceptions
class OrderCreationException(Exception):
pass
class OrderNotFound(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
f"Order {order_id} was not found in db "
"to emit."
)
class ReceiptGenerationFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
"Error found during emission! "
f"Order: {order_id}"
)
class ReceiptEmissionFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
"Emission failed! "
f"Order: {order_id} "
)
### Main class
class OrderService:
def emit(self, order_id: str) -> dict:
try:
...
except OrderNotFound:
logger.exception("We got a database exception")
raise
except ReceiptGenerationFailed:
logger.exception("We got a problem generating the receipt")
raise
except ReceiptEmissionFailed:
logger.exception("Unable to emit the receipt")
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Наконец-то мои глаза чувствуют облегчение. Поменьше повторений, пожалуйста! Примите к сведению, что рекомендуемый способ выглядит так, как я написал его выше: logger.exception(«ЛЮБОЕ СООБЩЕНИЕ»). Вам даже не нужно передавать исключение, поскольку его наличие уже подразумевается. Кроме того, кастомное сообщение, которое мы определили в каждом исключении с идентификатором order_id, будет отображаться в логах, поэтому вам не нужно повторяться и не нужно оперировать внутренними данными об исключениях.
Вот пример вывода ваших логов:
❯ python3 testme.py
Unable to emit the receipt # <<-- My log message
Traceback (most recent call last):
File "/path/testme.py", line 19, in <module>
tryme()
File "/path/testme.py", line 14, in tryme
raise ReceiptEmissionFailed(order_id)
ReceiptEmissionFailed: Emission failed! Order: 10 # <<-- My exception message
Теперь всякий раз, когда я получаю это исключение, сообщение уже ясно и понятно, и мне не нужно помнить о логировании order_id, который я сгенерировал.
Последнее улучшение: упрощение
После более детального рассмотрения нашего окончательного кода, он кажется лучше, теперь его легко читать и поддерживать.
Но управляет ли OrderService бизнес-логикой? Я не думаю, что это сервис в общем смысле. Он больше похож на координацию вызовов настоящих сервисов обеспечивающих бизнес-логику, которая выглядит получше, чем паттерн facade.
Кроме того, можно заметить, что он запрашивает данные у status_service, чтобы что-то с ними сделать. (Что, на этот раз, действительно разрушает идею «Говори, а не спрашивай»).
Перейдем к упрощению.
class OrderFacade: # Renamed to match what it actually is
def emit(self, order_id: str) -> dict:
try:
# NOTE: info logging still happens inside
status_service.ensure_order_unlocked(order_id)
receipt_note = receipt_service.create(order_id)
broker.emit_receipt_note(receipt_note)
order_status = status_service.get_order_status(order_id)
except OrderAlreadyInProgress as e:
# New block
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": e.order_status.value}
except OrderAlreadyEmitted as e:
# New block
logger.info(f"Aborting emission because it already happened! {e}")
return {"order_id": order_id, "order_status": e.order_status.value}
except OrderNotFound:
logger.exception("We got a database exception")
raise
except ReceiptGenerationFailed:
logger.exception("We got a problem generating the receipt")
raise
except ReceiptEmissionFailed:
logger.exception("Unable to emit the receipt")
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Мы только что создали новый метод ensure_order_unlocked для нашего status_service, который теперь отвечает за создание исключений/логирование в случае, если что-то идет не так.
Хорошо, а теперь скажите, насколько легче теперь стало это читать?
Я могу понять все return при беглом просмотре. Я знаю, что происходит, когда все идет хорошо, и как крайние случаи могут привести к разным результатам. И все это без прокрутки взад-вперед.
Теперь этот код такой же простой, каким (в основном) должен быть любой код.
Обратите внимание, что я решил вывести объект исключения e в логах, поскольку под капотом он будет запускать str(e), который вернет сообщение об исключении. Я подумал, что было бы полезно говорить подробно, поскольку мы не используем log.exception для этого блока, поэтому сообщение об исключении не будет отображаться.
Теперь давайте разберемся с некоторыми хитростями, которые помогут вам сделать код понятным для чтения и простым в обслуживании.
Эффективное создание исключений
Всегда классифицируйте свои исключения через базовое и расширяйте все конкретные исключения от него. С помощью этой полезной практики вы можете переиспользовать логику для связанного кода.
Исключения – это объекты, которые несут в себе информацию, поэтому не стесняйтесь добавлять кастомные атрибуты, которые могут помочь вам понять, что происходит. Не позволяйте своему бизнес-коду учить вас тому, как он должен быть построен, ведь с таким количеством сообщений и деталей потерять себя становится трудно.
# Base category exception
class OrderCreationException(Exception):
pass
# Specific error with custom message. Order id is required.
class OrderNotFound(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id # custom property
super().__init__(
f"Order {order_id} was not found in db "
f"to emit."
)
# Specific error with custom message. Order id is required.
class ReceiptGenerationFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id # custom property
super().__init__(
"Error found during emission! "
f"Order: {order_id}"
)
В примере выше я мог бы выйти за рамки и расширить базовый класс, чтобы всегда получать order_id, если мне это нужно. Этот совет поможет сохранить код сухим, поскольку мне не нужно быть многословным при создании исключений. Так можно использовать всего лишь одну переменную.
def func1(order_id):
raise OrderNotFound(order_id)
# instead of raise OrderNotFound(f"Can't find order {order_id}")
def func2(order_id):
raise OrderNotFound(order_id)
# instead of raise OrderNotFound(f"Can't find order {order_id}")
В тестировании также будет больше смысла, поскольку я могу сделать assert order_id через строку.
assert e.order_id == order_id
# instead of assert order_id in str(e)
Ловим и создаем исключения эффективно
Еще одна вещь, которую люди часто делают неправильно – это отлавливают и повторно создают исключения.
Согласно PEP 3134 Python, делать нужно следующим образом.
Повторное создание исключения
Обычной инструкции raise более чем достаточно.
try:
...
except CustomException as ex:
# do stuff (e.g. logging)
raise
Создание одного исключения из другого
Этот вариант особо актуален, поскольку он сохраняет всю трассировку стека и помогает вашей команде отлаживать основные проблемы.
try:
...
except CustomException as ex:
raise MyNewException() from ex
Эффективное логирование исключений
Еще один совет, который не позволит вам быть слишком многословным.
Используйте logger.exception
Вам не нужно логировать объект исключения. Функция exception логгера предназначена для использования внутри блоков except. Она уже обрабатывает трассировку стека с информацией о выполнении и отображает, какое исключение вызвало ее, с сообщением, установленном на уровне ошибки!
try:
...
except CustomException:
logger.exception("custom message")
А что, если это не ошибка?
Если по какой-то причине вы не хотите логировать исключение как ошибку, то возможно, это предупреждение или просто информация, как было показано выше.
Вы можете принять решение установить exc_info в True, если хотите сохранить трассировку стека. Кроме того, было бы неплохо использовать объект исключения внутри сообщения.
Источники
Документация Python:
· Python logging.logger.exception
· Python PEP 3134
Принципы и качество кода:
· Говори, а не спрашивай
· Паттерн facade
· Blob
— РЕГИСТРАЦИЯ
Данный урок посвящен исключениям и работе с ними. Основное внимание уделено понятию исключения в языках программирования, обработке исключений в Python, их генерации и созданию пользовательских исключений.
Исключения в языках программирования
Исключениями (exceptions) в языках программирования называют проблемы, возникающие в ходе выполнения программы, которые допускают возможность дальнейшей ее работы в рамках основного алгоритма. Типичным примером исключения является деление на ноль, невозможность считать данные из файла (устройства), отсутствие доступной памяти, доступ к закрытой области памяти и т.п. Для обработки таких ситуаций в языках программирования, как правило, предусматривается специальный механизм, который называется обработка исключений (exception handling).
Исключения разделяют на синхронные и асинхронные. Синхронные исключения могут возникнуть только в определенных местах программы. Например, если у вас есть код, который открывает файл и считывает из него данные, то исключение типа “ошибка чтения данных” может произойти только в указанном куске кода. Асинхронные исключения могут возникнуть в любой момент работы программы, они, как правило, связаны с какими-либо аппаратными проблемами, либо приходом данных. В качестве примера можно привести сигнал отключения питания.
В языках программирования чаще всего предусматривается специальный механизм обработки исключений. Обработка может быть с возвратом, когда после обработки исключения выполнение программы продолжается с того места, где оно возникло. И обработка без возврата, в этом случае, при возникновении исключения, осуществляется переход в специальный, заранее подготовленный, блок кода.
Различают структурную и неструктурную обработку исключений. Неструктурная обработка предполагает регистрацию функции обработчика для каждого исключения, соответственно данная функция будет вызвана при возникновении конкретного исключения. Для структурной обработки язык программирования должен поддерживать специальные синтаксические конструкции, которые позволяют выделить код, который необходимо контролировать и код, который нужно выполнить при возникновении исключительной ситуации.
В Python выделяют два различных вида ошибок: синтаксические ошибки и исключения.
Синтаксические ошибки в Python
Синтаксические ошибки возникают в случае если программа написана с нарушениями требований Python к синтаксису. Определяются они в процессе парсинга программы. Ниже представлен пример с ошибочным написанием функции print.
>>> for i in range(10): prin("hello!") Traceback (most recent call last): File "<pyshell#2>", line 2, in <module> prin("hello!") NameError: name 'prin' is not defined
Исключения в Python
Второй вид ошибок – это исключения. Они возникают в случае если синтаксически программа корректна, но в процессе выполнения возникает ошибка (деление на ноль и т.п.). Более подробно про понятие исключения написано выше, в разделе “исключения в языках программирования”.
Пример исключения ZeroDivisionError, которое возникает при делении на 0.
>>> a = 10 >>> b = 0 >>> c = a / b Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> c = a / b ZeroDivisionError: division by zero
В Python исключения являются определенным типом данных, через который пользователь (программист) получает информацию об ошибке. Если в коде программы исключение не обрабатывается, то приложение останавливается и в консоли печатается подробное описание произошедшей ошибки с указанием места в программе, где она произошла и тип этой ошибки.
Иерархия исключений в Python
Существует довольно большое количество встроенных типов исключений в языке Python, все они составляют определенную иерархию, которая выглядит так, как показано ниже.
BaseException
+– SystemExit
+– KeyboardInterrupt
+– GeneratorExit
+– Exception
+– StopIteration
+– StopAsyncIteration
+– ArithmeticError
| +– FloatingPointError
| +– OverflowError
| +– ZeroDivisionError
+– AssertionError
+– AttributeError
+– BufferError
+– EOFError
+– ImportError
+– ModuleNotFoundError
+– LookupError
| +– IndexError
| +– KeyError
+– MemoryError
+– NameError
| +– UnboundLocalError
+– OSError
| +– BlockingIOError
| +– ChildProcessError
| +– ConnectionError
| | +– BrokenPipeError
| | +– ConnectionAbortedError
| | +– ConnectionRefusedError
| | +– ConnectionResetError
| +– FileExistsError
| +– FileNotFoundError
| +– InterruptedError
| +– IsADirectoryError
| +– NotADirectoryError
| +– PermissionError
| +– ProcessLookupError
| +– TimeoutError
+– ReferenceError
+– RuntimeError
| +– NotImplementedError
| +– RecursionError
+– SyntaxError
| +– IndentationError
| +– TabError
+– SystemError
+– TypeError
+– ValueError
| +– UnicodeError
| +– UnicodeDecodeError
| +– UnicodeEncodeError
| +– UnicodeTranslateError
+– Warning
+– DeprecationWarning
+– PendingDeprecationWarning
+– RuntimeWarning
+– SyntaxWarning
+– UserWarning
+– FutureWarning
+– ImportWarning
+– UnicodeWarning
+– BytesWarning
+– ResourceWarning
Как видно из приведенной выше схемы, все исключения являются подклассом исключения BaseException. Более подробно об иерархии исключений и их описании можете прочитать здесь.
Обработка исключений в Python
Обработка исключений нужна для того, чтобы приложение не завершалось аварийно каждый раз, когда возникает исключение. Для этого блок кода, в котором возможно появление исключительной ситуации необходимо поместить во внутрь синтаксической конструкции try…except.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except Exception as e: print("Error! " + str(e)) print("stop")
В приведенной выше программе возможных два вида исключений – это ValueError, возникающее в случае, если на запрос программы “введите число”, вы введете строку, и ZeroDivisionError – если вы введете в качестве числа 0.
Вывод программы при вводе нулевого числа будет таким.
start input number: 0 Error! stop
Если бы инструкций try…except не было, то при выбросе любого из исключений программа аварийно завершится.
print("start") val = int(input(“input number: “)) tmp = 10 / val print(tmp) print("stop")
Если ввести 0 на запрос приведенной выше программы, произойдет ее остановка с распечаткой сообщения об исключении.
start
input number: 0
Traceback (most recent call last):
File “F:/work/programming/python/devpractice/tmp.py”, line 3, in <module>
tmp = 10 / val
ZeroDivisionError: division by zero
Обратите внимание, надпись stop уже не печатается в конце вывода программы.
Согласно документу по языку Python, описывающему ошибки и исключения, оператор try работает следующим образом:
- Вначале выполняется код, находящийся между операторами try и except.
- Если в ходе его выполнения исключения не произошло, то код в блоке except пропускается, а код в блоке try выполняется весь до конца.
- Если исключение происходит, то выполнение в рамках блока try прерывается и выполняется код в блоке except. При этом для оператора except можно указать, какие исключения можно обрабатывать в нем. При возникновении исключения, ищется именно тот блок except, который может обработать данное исключение.
- Если среди except блоков нет подходящего для обработки исключения, то оно передается наружу из блока try. В случае, если обработчик исключения так и не будет найден, то исключение будет необработанным (unhandled exception) и программа аварийно остановится.
Для указания набора исключений, который должен обрабатывать данный блок except их необходимо перечислить в скобках (круглых) через запятую после оператора except.
Если бы мы в нашей программе хотели обрабатывать только ValueError и ZeroDivisionError, то программа выглядела бы так.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except(ValueError, ZeroDivisionError): print("Error!") print("stop")
Или так, если хотим обрабатывать ValueError, ZeroDivisionError по отдельность, и, при этом, сохранить работоспособность при возникновении исключений отличных от вышеперечисленных.
print("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except ValueError: print("ValueError!") except ZeroDivisionError: print("ZeroDivisionError!") except: print("Error!") print("stop")
Существует возможность передать подробную информацию о произошедшем исключении в код внутри блока except.
rint("start") try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except ValueError as ve: print("ValueError! {0}".format(ve)) except ZeroDivisionError as zde: print("ZeroDivisionError! {0}".format(zde)) except Exception as ex: print("Error! {0}".format(ex)) print("stop")
Использование finally в обработке исключений
Для выполнения определенного программного кода при выходе из блока try/except, используйте оператор finally.
try: val = int(input("input number: ")) tmp = 10 / val print(tmp) except: print("Exception") finally: print("Finally code")
Не зависимо от того, возникнет или нет во время выполнения кода в блоке try исключение, код в блоке finally все равно будет выполнен.
Если необходимо выполнить какой-то программный код, в случае если в процессе выполнения блока try не возникло исключений, то можно использовать оператор else.
try: f = open("tmp.txt", "r") for line in f: print(line) f.close() except Exception as e: print(e) else: print("File was readed")
Генерация исключений в Python
Для принудительной генерации исключения используется инструкция raise.
Самый простой пример работы с raise может выглядеть так.
try: raise Exception("Some exception") except Exception as e: print("Exception exception " + str(e))
Таким образом, можно “вручную” вызывать исключения при необходимости.
Пользовательские исключения (User-defined Exceptions) в Python
В Python можно создавать собственные исключения. Такая практика позволяет увеличить гибкость процесса обработки ошибок в рамках той предметной области, для которой написана ваша программа.
Для реализации собственного типа исключения необходимо создать класс, являющийся наследником от одного из классов исключений.
class NegValException(Exception): pass try: val = int(input("input positive number: ")) if val < 0: raise NegValException("Neg val: " + str(val)) print(val + 10) except NegValException as e: print(e)
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.

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