Узнать и забыть: 4 антипрактики обработки ошибок в Python

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

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

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

Обработка ошибок  —  важнейший этап создания приложения, благодаря которому код становится более удобным в обслуживании. 

Для вызова исключения в 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

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

Благодарю за внимание! Успехов в программировании! 

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Jerry Ng: Stop Using Exceptions Like This in Python

Предыдущая статья6 полезных библиотек JavaScript
Следующая статьяNeuralHash от Apple: принцип работы и слабые места