Люди, которые пишут код, часто воспринимают работу с исключениями как необходимое зло. Но освоение системы обработки исключений в Python способно повысить профессиональный уровень программиста, сделать его эффективнее. В этом материале я разберу следующие темы, изучение которых поможет всем желающим раскрыть потенциал Python через разумный подход к обработке исключений:
Что такое обработка исключений?
Разница между оператором
if
и обработкой исключений.Использование разделов
else
иfinally
блокаtry-except
для организации правильного обращения с ошибками.Определение пользовательских исключений.
Рекомендации по обработке исключений.
Что такое обработка исключений?
Обработка исключений — это процесс написания кода для перехвата и обработки ошибок или исключений, которые могут возникать при выполнении программы. Это позволяет разработчикам создавать надёжные программы, которые продолжают работать даже при возникновении неожиданных событий или ошибок. Без системы обработки исключений подобное обычно приводит к фатальным сбоям.
Когда возникают исключения — Python выполняет поиск подходящего обработчика исключений. После этого, если обработчик будет найден, выполняется его код, в котором предпринимаются уместные действия. Это может быть логирование данных, вывод сообщения, попытка восстановить работу программы после возникновения ошибки. В целом можно сказать, что обработка исключения помогает повысить надёжность Python-приложений, улучшает возможности по их поддержке, облегчает их отладку.
Различия между оператором if и обработкой исключений
Главные различия между оператором if
и обработкой исключений в Python произрастают из их целей и сценариев использования.
Оператор if
— это базовый строительный элемент структурного программирования. Этот оператор проверяет условие и выполняет различные блоки кода, основываясь на том, истинно проверяемое условие или ложно. Вот пример:
temperature = int(input("Please enter temperature in Fahrenheit: "))
if temperature > 100:
print("Hot weather alert! Temperature exceeded 100°F.")
elif temperature >= 70:
print("Warm day ahead, enjoy sunny skies.")
else:
print("Bundle up for chilly temperatures.")
Обработка исключений, с другой стороны, играет важную роль в написании надёжных и отказоустойчивых программ. Эта роль раскрывается через работу с неожиданными событиями и ошибками, которые могут возникать во время выполнения программы.
Исключения используются для подачи сигналов о проблемах и для выявления участков кода, которые нуждаются в улучшении, отладке, или в оснащении их дополнительными механизмами для проверки ошибок. Исключения позволяют Python достойно справляться с ситуациями, в которых возникают ошибки. В таких ситуациях исключения дают возможность продолжать выполнение скрипта вместо того, чтобы резко его останавливать.
Рассмотрим следующий код, демонстрирующий пример того, как можно реализовать обработку исключений и улучшить ситуацию с потенциальными отказами, связанными с делением на ноль:
# Определение функции, которая пытается поделить число на ноль
def divide(x, y):
result = x / y
return result
# Вызов функции divide с передачей ей x=5 и y=0
result = divide(5, 0)
print(f"Result of dividing {x} by {y}: {result}")
Вывод:
Traceback (most recent call last):
File "<stdin>", line 8, in <module>
ZeroDivisionError: division by zero attempted
После того, как было сгенерировано исключение, программа, не дойдя до инструкции print
, сразу же прекращает выполняться.
Вышеописанное исключение можно обработать, обернув вызов функции divide
в блок try-except
:
# Определение функции, которая пытается поделить число на ноль
def divide(x, y):
result = x / y
return result
# Вызов функции divide с передачей ей x=5 и y=0
try:
result = divide(5, 0)
print(f"Result of dividing {x} by {y}: {result}")
except ZeroDivisionError:
print("Cannot divide by zero.")
Вывод:
Cannot divide by zero.
Сделав это, мы аккуратно обработали исключение ZeroDivisionError
, предотвратили аварийное завершение остального кода из-за необработанного исключения.
Подробности о других встроенных Python-исключениях можно найти здесь.
Использование разделов else и finally блока try-except для организации правильного обращения с ошибками
При работе с исключениями в Python рекомендуется включать в состав блоков try-except
и раздел else
, и раздел finally
. Раздел else
позволяет программисту настроить действия, производимые в том случае, если при выполнении кода, который защищают от проблем, не было вызвано исключений. А раздел finally
позволяет обеспечить обязательное выполнение неких заключительных операций, вроде освобождения ресурсов, независимо от факта возникновения исключений (вот и вот — полезные материалы об этом).
Например — рассмотрим ситуацию, когда нужно прочитать данные из файла и выполнить какие-то действия с этими данными. Если при чтении файла возникнет исключение — программист может решить, что надо залогировать ошибку и остановить выполнение дальнейших операций. Но в любом случае файл нужно правильно закрыть.
Использование разделов else
и finally
позволяет поступить именно так — обработать данные обычным образом в том случае, если исключений не возникло, либо обработать любые исключения, но, как бы ни развивались события, в итоге закрыть файл. Без этих разделов код страдал бы уязвимостями в виде утечки ресурсов или неполной обработки ошибок. В результате оказывается, что else
и finally
играют важнейшую роль в создании устойчивых к ошибкам и надёжных программ.
try:
# Открытие файла в режиме чтения
file = open("file.txt", "r")
print("Successful opened the file")
except FileNotFoundError:
# Обработка ошибки, возникающей в том случае, если файл не найден
print("File Not Found Error: No such file or directory")
exit()
except PermissionError:
# Обработка ошибок, связанных с разрешением на доступ к файлу
print("Permission Denied Error: Access is denied")
else:
# Всё хорошо - сделать что-то с данными, прочитанными из файла
content = file.read().decode('utf-8')
processed_data = process_content(content)
# Прибираемся после себя даже в том случае, если выше возникло исключение
finally:
file.close()
В этом примере мы сначала пытаемся открыть файл file.txt
для чтения (в подобной ситуации можно использовать выражение with
, которое гарантирует правильное автоматическое закрытие объекта файла после завершения работы). Если в процессе выполнения операций файлового ввода/вывода возникают ошибки FileNotFoundError
или PermissionError
— выполняются соответствующие разделы except
. Здесь, ради простоты, мы лишь выводим на экран сообщения об ошибках и выходим из программы в том случае, если файл не найден.
В противном случае, если в блоке try
исключений не возникло, мы продолжаем работу, обрабатывая содержимое файла в ветви else
. И наконец — выполняется «уборка» — файл закрывается независимо от возникновения исключения. Это обеспечивает блок finally
(подробности смотрите здесь).
Применяя структурированный подход к обработке исключений, напоминающий вышеописанный, можно поддерживать свой код в хорошо организованном состоянии и обеспечивать его читабельность. При этом код будет рассчитан на борьбу с потенциальными ошибками, которые могут возникнуть при взаимодействии с внешними системами или входными данными.
Определение пользовательских исключений
В Python можно определять пользовательские исключения путём создания подклассов встроенного класса Exception
или любых других классов, являющихся прямыми наследниками Exception
.
Для того чтобы определить собственное исключение — нужно создать новый класс, являющийся наследником одного из подходящих классов, и оснастить этот класс атрибутами, соответствующими нуждам программиста. Затем новый класс можно использовать в собственном коде, работая с ним так же, как работают со встроенными классами исключений.
Вот пример определения пользовательского исключения, названного InvalidEmailAddress
:
class InvalidEmailAddress(ValueError):
def __init__(self, message):
super().__init__(message)
self.msgfmt = message
Это исключение является наследником ValueError
. Его конструктор принимает необязательный аргумент message
(по умолчанию он устанавливается в значение invalid email address
).
Вызвать это исключение можно в том случае, если в программе встретился адрес электронной почты, имеющий некорректный формат:
def send_email(address):
if isinstance(address, str) == False:
raise InvalidEmailAddress("Invalid email address")
# Отправка электронного письма
Теперь, если функции send_email()
будет передана строка, содержащая неправильно оформленный адрес, то, вместо сообщения стандартной ошибки TypeError
, будет выдано настроенное заранее сообщение об ошибке, которое чётко указывает на возникшую проблему. Например, это может выглядеть так:
>>> send_email(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/path/to/project/main.py", line 8, in send_email
raise InvalidEmailAddress("Invalid email address")
InvalidEmailAddress: Invalid email address
Рекомендации по обработке исключений
Вот несколько рекомендаций, относящихся к обработке ошибок в Python:
Проектируйте код в расчёте на возможное возникновение ошибок. Заранее планируйте устройство кода с учётом возможных сбоев и проектируйте программы так, чтобы они могли бы достойно обрабатывать эти сбои. Это означает — предугадывать возможные пограничные случаи и реализовывать подходящие обработчики ошибок.
Используйте содержательные сообщения об ошибках. Сделайте так, чтобы программа выводила бы, на экран, или в файл журнала, подробные сообщения об ошибках, которые помогут пользователям понять — что и почему пошло не так. Старайтесь не применять обобщённые сообщения об ошибках, наподобие
Error occurred
илиSomething bad happened
. Вместо этого подумайте об удобстве пользователя и покажите сообщение, в котором будет дан совет по решению проблемы или будет приведена ссылка на документацию. Постарайтесь соблюсти баланс между выводом подробных сообщений и перегрузкой пользовательского интерфейса избыточными данными.Минимизируйте побочные эффекты. Постарайтесь свести к минимуму последствия сбойных операций, изолируя проблемные разделы кода посредством конструкции
try-finally
илиtry
с использованиемwith
. Сделайте так, чтобы после выполнения кода, было ли оно удачным или нет, обязательно выполнялись бы «очистительные» операции.Тщательно тестируйте код. Обеспечьте корректное поведение обработчиков ошибок в различных сценариях использования программы, подвергнув код всеобъемлющему тестированию.
Регулярно выполняйте рефакторинг кода. Выполняйте рефакторинг фрагментов кода, подверженных ошибкам, чтобы улучшить их надёжность и производительность. Постарайтесь, чтобы ваша кодовая база была бы устроена по модульному принципу, чтобы её отдельные части слабо зависели бы друг от друга. Это позволяет независимым частям код самостоятельно эволюционировать, не оказывая негативного воздействия на другие его части.
Логируйте важные события. Следите за интересными событиями своего приложения, записывая сведения о них в файл журнала или выводя в консоль. Это поможет вам выявлять проблемы на ранних стадиях их возникновения, не тратя время на длительный анализ большого количества неструктурированных логов.
Итоги
Написание кода обработки ошибок — это неотъемлемая часть индустрии разработки ПО, и, в частности — разработки на Python. Это позволяет разработчикам создавать более надёжные и стабильные программы. Следуя индустриальным стандартам и рекомендациям по обработке исключений, разработчик может сократить время, необходимое на отладку кода, способен обеспечить написание качественных программ и сделать так, чтобы пользователям было бы приятно работать с этими программами.
О, а приходите к нам работать? ???? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (20)
Satim
22.05.2023 09:21+1Сколько статей не читаю про исключения почти везде пытаются использовать исключения как замену if. Тот же пример с делением на 0 приводится везде, но ведь там логичнее использовать if и код будет более понятным разве нет? Пока видел всего несколько мест где действительно требуются исключения.
1) Это если вы пишете системный код с которым работает другой код(всяческие библиотеки и прочее)
2) У вас глубоко вложенная логика работы программы когда множество функций вызываются внутри друг друга и чтобы не передавать результат работы одной функции сквозь все дерево вызовов проще кинуть исключение которое обработать в главном методе.
Но никто не приводит таких примеров в статье, все ограничиваются поверхностным объяснением и очень примитивными примерами в которых объясняется синтаксис исключений, но не суть.
CrazyElf
22.05.2023 09:21Ну или вот вы хотите открыть и прочитать файл. Что может пойти не так? Да сразу много чего. Логично ли все эти возможные косяки проверять
if
-ами? Нет. Гораздо проще поймать исключение и там уже будет написана довольно локализованная проблема - такого файла нет или у вас нет прав его читать или файл лежит на сетевом диске, который отвалился, или ещё что-то. Ну и в целом да, вы никогда не знаете, какой уровень контроля окажется в итоге нужен при вызове метода. Вот обнаружили вы деление на0
с помощьюif
- и что дальше то? При использовании исключений всё понятно - возникает исключение и летит наверх до того уровня, где его считают нужным поймать. И не нужно придумывать какие-то отдельные конвенции - что же вы должны делать, что возвращать, если у вас функция деления и вы обнаружили в знаменателе ноль.Satim
22.05.2023 09:21-1Ну а что может пойти не так при работе с файлом?
Прав нет, вызвали метод проверки прав перед открытием файла показали сообщение пользователю что прав нет.
Файла нет, показали сообщение пользователю что файла нет.
Код ясный и понятный, нормальные человеческие сообщения пользователю. Или вы предлагаете пользователю отдавать вот это
"FileNotFoundError: [Errno 2] No such file or directory: 'file.txt'"
Я предпочту написать нормально сообщение + залогировать что такой то пользователь пытался получить доступ к несуществующему файлу.
"Файл с именем 'file.txt' не найден. Обратитесь в тех поддержку -7(999)555-55-55."
CrazyElf
22.05.2023 09:21Вы не в курсе, что можно ловить не общий
Exception
, а конкретные исключения типа того жеFileNotFoundError
и выдавать понятные пользователю сообщения в этом случае? Это всё-равно будет проще, чем проверять все косяки вручную, да и нет никакой гарантии, что, например, файл не удалят или не поменяют ему права между моментом, когда вы его проверите черезif
и моментом когда откроете файл в программе. И поэтому вам всё-равно придётся ловить исключения и как-то их обрабатывать.Satim
22.05.2023 09:21-1В курсе. Предлагаете все конкретные исключения в except'e отлавливать?
Я вот предпочту вообще до исключения не доводить. Ну и зачем пытаться читать файл с диска если прав нет. В том смысле что зачем пытаться выполнить код, если заранее известно что он упадет с исключением?
Я не говорю что исключения совсем не нужны, я пытаюсь сказать что каждому инструменту свое время и место. В большинстве случаев лучше использовать if чем пытаться отовсюду ловить исключения.
CrazyElf
22.05.2023 09:21+2То есть вы предлагаете программистам делать двойную работу? )) Сначала проверять что-то через
if
, а потом ещё ловить соответствующийexception
. Прелестно, прелестно )) И да, хороший стиль - именно ловить в блокеtry-except
все конкретныеexception
, которые вы ожидаете, что вылетят и вы знаете, как вы хотите на них реагировать. А те, с которыми не знаете что делать - не ловить, пусть на них выше по стеку кто-то реагирует. Это в любом руководстве написано.Satim
22.05.2023 09:21+1Вы видимо совсем не читаете мои сообщения, либо не хотите понять. Я нигде не говорил что нужно делать двойную работу. Нужно грамотно организовывать код, где-то сделать проверку if где то обработку исключения. Но не нужно подменять одно другим.
CrazyElf
22.05.2023 09:21Я всё прекрасно понимаю. У вас какой опыт вообще в программировании, сколько лет и какого грейда?
Я же уже приводил пример, что между проверкой на всякое и открытием файла его статус может успеть поменяться и вам всё-равно придётся проверять те исключения, которых вы пытаетесь избежать.
Vindicar
22.05.2023 09:21+4Во-первых, даже если есть права, файл может быть занят. Или файл уже открыт, но пользователь выдернул флэшку/отключил соединение с сетевым диском. Часто бывает так, что не попытавшись выполнить операцию - не поймёшь, выполнима ли она.
Во-вторых, в интервал времени между проверкой и открытием файла может произойти много всякого. Вон, был пример где службу обновления Steam обманывали, подсунув свой код, потому что она сначала открывала файл для проверки подписи, а потом закрывала и переоткрывала для выполнения.
Так что от неожиданных проблем никуда не деться. Стоит ли городить отдельный огород для проблем ожидаемых, в таком случае?
thevlad
22.05.2023 09:21+2Проблема в ифами и кодами возврата, в том, что их никто не проверяет. А если и проверять тотально, то с увеличением вложенности это выливается в лапшу из ифов проверяющих коды возврата на каждом уровне.
Конечно, есть разные подходы, на тех же плюсах, гугл пишет с отключенными исключениями Но это тоже палка о двух концах, если вам нужна надежная система на плюсах, в которой точно не текут ресурсы или другие неожиданные проблемы, то проще писать без исключений. Если же просто надо красиво завершится с дампом и стэк трейсом, или понятным сообщением, то проще писать с исключениями.
Satim
22.05.2023 09:21Так я пишу что для ситуации глубокой вложенности кода проще кинуть исключение и ловить его выше, чем морочится с ифами. Вот только никто такого примера не приводит в статьях про исключения.
vagon333
22.05.2023 09:21+1Встречал оба подхода — if и exception.
Выбор подхода — вопрос философии разработки или корпоративных стандартов.В своих проектах использую:
- if во всех случаях, когда возможна ошибка
- класс aggregated_parameter, который включает класс messages, в котором есть список errors для if ошибок.
- в ключевых местах кода проверяю aggregated_parameter.messages.errors.count и выхожу, с вежливым объяснением ошибки, которая возвращается пользователю (aggregated_parameter.messages), без кишок stack trace.
В реальности, возвращается пользователю errors, warnings и information сообщения.
Например, information нужен когда в работе бизнес логики выполнена задача, которую необходимо озвучить пользователю для прозрачности процессов.
one-eyed-j
22.05.2023 09:21+1Автор, похоже, писал примеры без понимания того, как реально работают исключения. В частности в примере с открытием файла клауза
finally
написано просто неправильно. Еслиopen()
сгенерировала ошибку, то переменнаяfile
вообще не будет определена. В результате будет сгенерировано ещё одно исключениеNameError
. Уж лучше читайте учебники, чем блоггеров с Medium.
BigLY
22.05.2023 09:21+1Важно отличать подходы EAFP(Проще просить прощения, чем разрешения) и LBYL(Смотри, прежде чем прыгать). LBYL стоит применять когда ты работаешь со статическими данными, например: обработка результата функции, проверка ключа в словаре и т.д. EAFP используется когда работа ведется с динамически изменяемой средой, например: проверка доступности удаленного сервера не дает тебе гарантию, что сервер не сломается сразу после ответа на проверку, или файл не будет удален после открытия в приложении и т.д.
Хороший пример EAFP метод raise_for_status библиотеки requests, который поднимает ошибку, а не генерирует ответ с результатом валидации статуса ответа.
timur4u
Логичнее была бы инструкция then, чем else.