Представьте, что вы создаете приложение для ведения блогов. В его обычном режиме пользователи могут регистрироваться и писать посты.
Как правило, исключение (ошибка) возникает в случае непредвиденного сбоя в работе приложения. Например, при регистрации пользователь вводит уже существующий на сайте почтовый адрес.
Обработка исключений предполагает принятие разработчиком мер в отношении произошедшего сбоя. Например, в данной ситуации можно просто вернуть пользователю сообщение об ошибке, записать ее в систему логирования и т. д.
Обработка ошибок — важнейший этап создания приложения, благодаря которому код становится более удобным в обслуживании.
Для вызова исключения в Python достаточно выполнить следующую инструкцию:
raise ValueError("Something has gone wrong.")
В статье мы рассмотрим как рекомендованные практики для обработки ошибок, так и те, которые лучше избегать.
1. Никаких пустых инструкций except
Первое и главное правило — не применять инструкцию except
без указания типа обрабатываемого исключения (так называемую пустую инструкцию), поскольку она не предоставляет для изучения какой-либо объект исключения.
Более того, она перехватывает все исключения, включая и те, которые вообще не нужны: SystemExit
или KeyboardInterrupt
.
Приведем пример пустой инструкции except
, который лучше исключить из своей практики:
# НИКОГДА не используйте пустое исключение:
try:
return fetch_all_books()
except: # !!!
raise
Почему не следует перехватывать каждое исключение?
Перехват каждого исключения может вызвать сбой в работе приложения, а мы так и не узнаем настоящую причину. Страшно даже об этом подумать, особенно когда дело касается отладки.
К примеру мы создаем функциональность, позволяющую пользователю загружать файл PDF, и размещаем в данном блоке кода обобщенную конструкцию try-catch
.
В следующем коде мы перехватываем обобщенное исключение, которое сообщает, что файл не найден, но не раскрывает сути имеющейся проблемы:
def upload_proof_of_address_doc(file):
try:
result = upload_pdf_to_s3(file)
if not result:
raise Exception("File not found!")
except:
raise
Независимо от фактической причины (конечная точка предназначена только для чтения; отсутствие прав доступа или неверный тип файла) любому, кто столкнулся с проблемой загрузки, вернется ошибка ”File not found!"
.
Теперь представьте, что файл загрузился, но вместо первоначального пользователь загрузил файл изображения. При этом он по-прежнему продолжает получать ту же самую ошибку при каждой очередной попытке. В итоге все заканчивается выплесками раздражения и разочарованием в приложении.
Во всех трех упомянутых случаях для решения проблемы рекомендованы конкретные классы исключений.
2. Никаких вызовов Exception
Согласно второму правилу следует избегать вызова обобщенного класса Exception
в Python, поскольку это приводит к сокрытию ошибок.
Обратимся к примеру, которому не место в реальной практики:
# НЕ следует вызывать обобщенный Exception:
def get_book_List():
try:
if not fetch_books():
raise Exception("This exception will not be caught by specific catch") # !!!
except ValueError as e:
print("This doesn't catch Exception")
raise
get_book_List()
# Exception: общие исключения не перехватываются при обработке конкретных типов исключений
Из всех разнообразных вариантах плохого кода, данный антипаттерн, известный как скрытие ошибок, самый худший.
По своему личному опыту могу сказать, что никакой другой шаблон так не снижает производительность разработки, как этот.
3. Никаких инструкций except с Exception
Как правило, разработчики предпочитают обертывать код функций блоком try-except
, поскольку, как известно, в этом случае всегда есть гарантия выброса исключений.
Перестраховаться никогда не помешает, верно?
# НЕ перехватывайте с помощью обобщенного класса Exception
def fetch_all_books():
try:
if not fetch_all_books():
raise DoesNotExistError("No books found!")
except Exception as e: # !!!
print(e)
raise
Однако, действуя таким образом, разработчики обычно перехватывают исключения с помощью обобщенного класса BaseException
или Exception
.
Но при таком сценарии перехватывается всё, включая исключения, после которых не будет возможности или смысла восстановиться.
Планировать, планировать и еще раз планировать
Всегда рекомендуется заранее предугадывать и продумывать возможные сбои и выброшенные исключения.
Например, мы работаем с вызовом базы данных для получения профиля пользователя с указанием электронной почты. При этом мы должны учесть вероятность отсутствия адреса и обработать данный случай соответствующим образом.
В рассматриваемом сценарии можно вызвать пользовательскую ошибку UserDoesNotExist
и отправить запрос на повторную попытку, позволяя приложению восстановиться после исключения.
Приведем простой пример вызова исключения, определенного пользователем, в Python:
class UserDoesNotExist(Exception):
"""Raised when user does not exist"""
pass
Перед написанием пользовательских исключений всегда следует проверять наличие в рабочей библиотеке встроенных исключений, отвечающих требованиям сценария использования.
Короче говоря, мы должны перехватывать только интересующие нас ошибки с помощью конкретного типа Exception
, семантически соответствующего случаю.
Рассмотрим улучшенный пример правильной обработки исключений с добавлением конкретики:
# Выполняем:
def fetch_user_profile(id):
try:
user = get_user_profile(id)
if not user:
raise UserDoesNotExist("User does not exist.") # Вызываем конкретное исключение
except UserDoesNotExist as e: # Перехватываем его
logger.exception(e) # Записываем это исключение в лог
raise # Проcто raise
# raise UserDoesNotExist # Не следует этого делать из-за потери трассировки стека
Но, я не знаю, какие использовать исключения
Понятно, что подготовиться ко всем возможным исключениям практически невозможно.
В таких случаях советуют хотя бы перехватывать их с помощью класса Exception
, поскольку он не включает такие исключения, как GeneratorExist
, SystemExit
и KeyboardInterrupt
, приводящие к завершению работы приложения.
Я все-таки рекомендую не жалеть время на продумывание потенциально возможных вариантов, поскольку привычка перехватывать обобщенные исключения ни к чему хорошему не приведет.
И вот еще одно верное тому доказательство.
4. Никаких инструкций pass в блоке except
При разработке приложения в отношении определенных исключений можно не предпринимать никаких действий.
Однако самое худшее, что может сделать разработчик, выглядит следующим образом:
# НИКОГДА не игнорируйте пустую инструкцию except
try:
compute_combination_sum(value)
except:
pass
# НЕ делайте этого:
try:
compute_combination_sum(value)
except BaseException:
pass
Согласно данному коду мы не готовы к каким-либо исключениям, но тем не менее охотно их перехватываем.
Еще один недостаток игнорирования и перехвата Exception
(или пустой инструкции except
) в том, что при наличии двух ошибок в коде мы никогда не узнаем вторую из них. Сначала происходит перехват первой ошибки, за которым следует выход из блока try
.
Не предпринимая никаких действий в инструкции except
, мы показываем, что особо не готовы к перехватываемым исключениям. Возможно, это как раз время для переосмысления и рефакторинга.
Лучше логировать, чем игнорировать
Однако если ничего с исключением делать не нужно, то, по крайней мере, следует его конкретизировать и записать в лог.
import logging
logger = logging.getLogger(__name__)
try:
parse_number(value)
except ValueError as e:
logger.exception(e)
Можно рассмотреть варианты с включением кода восстановления и добавлением комментария, информирующего разработчиков о случае использования.
Суть в том, что следует избегать инструкций pass
в блоках except
, за исключением ситуаций, продиктованных осознанным намерением. Однако это плохой знак.
В итоге исключение должно быть записано в систему мониторинга. Так мы получим в распоряжение лог с информацией о произошедшем сбое.
Выводы
Обобщая все вышесказанное, еще раз сформулируем рекомендации:
- Никогда не задействуйте пустую инструкцию
except
. - Не вызывайте обобщенный класс
Exception
. - Не перехватывайте обобщенный
Exception
. - Воздерживайтесь от применения инструкции
pass
в блокахexcept
.
Зачастую лучше, чтобы при возникновении исключения приложение прекратило свою работу, а не продолжало функционировать странным и непредсказуемым способом. Самый оптимальный вариант — перехватывать только исключения, которые мы знаем и готовы обработать.
Благодарю за внимание! Успехов в программировании!
Читайте также:
- Жажда скорости: Python с расширениями С
- Отслеживание фокусированного времени с помощью Python
- Отладка кода на Python с помощью icecream
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Jerry Ng: Stop Using Exceptions Like This in Python