Люди, которые пишут код, часто воспринимают работу с исключениями как необходимое зло. Но освоение системы обработки исключений в 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:

  1. Проектируйте код в расчёте на возможное возникновение ошибок. Заранее планируйте устройство кода с учётом возможных сбоев и проектируйте программы так, чтобы они могли бы достойно обрабатывать эти сбои. Это означает — предугадывать возможные пограничные случаи и реализовывать подходящие обработчики ошибок.

  2. Используйте содержательные сообщения об ошибках. Сделайте так, чтобы программа выводила бы, на экран, или в файл журнала, подробные сообщения об ошибках, которые помогут пользователям понять — что и почему пошло не так. Старайтесь не применять обобщённые сообщения об ошибках, наподобие Error occurred или Something bad happened. Вместо этого подумайте об удобстве пользователя и покажите сообщение, в котором будет дан совет по решению проблемы или будет приведена ссылка на документацию. Постарайтесь соблюсти баланс между выводом подробных сообщений и перегрузкой пользовательского интерфейса избыточными данными.

  3. Минимизируйте побочные эффекты. Постарайтесь свести к минимуму последствия сбойных операций, изолируя проблемные разделы кода посредством конструкции try-finally или try с использованием with. Сделайте так, чтобы после выполнения кода, было ли оно удачным или нет, обязательно выполнялись бы «очистительные» операции.

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

  5. Регулярно выполняйте рефакторинг кода. Выполняйте рефакторинг фрагментов кода, подверженных ошибкам, чтобы улучшить их надёжность и производительность. Постарайтесь, чтобы ваша кодовая база была бы устроена по модульному принципу, чтобы её отдельные части слабо зависели бы друг от друга. Это позволяет независимым частям код самостоятельно эволюционировать, не оказывая негативного воздействия на другие его части.

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

Итоги

Написание кода обработки ошибок — это неотъемлемая часть индустрии разработки ПО, и, в частности — разработки на Python. Это позволяет разработчикам создавать более надёжные и стабильные программы. Следуя индустриальным стандартам и рекомендациям по обработке исключений, разработчик может сократить время, необходимое на отладку кода, способен обеспечить написание качественных программ и сделать так, чтобы пользователям было бы приятно работать с этими программами.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Комментарии (20)


  1. timur4u
    22.05.2023 09:21

    Логичнее была бы инструкция then, чем else.


  1. Satim
    22.05.2023 09:21
    +1

    Сколько статей не читаю про исключения почти везде пытаются использовать исключения как замену if. Тот же пример с делением на 0 приводится везде, но ведь там логичнее использовать if и код будет более понятным разве нет? Пока видел всего несколько мест где действительно требуются исключения.

    1) Это если вы пишете системный код с которым работает другой код(всяческие библиотеки и прочее)

    2) У вас глубоко вложенная логика работы программы когда множество функций вызываются внутри друг друга и чтобы не передавать результат работы одной функции сквозь все дерево вызовов проще кинуть исключение которое обработать в главном методе.

    Но никто не приводит таких примеров в статье, все ограничиваются поверхностным объяснением и очень примитивными примерами в которых объясняется синтаксис исключений, но не суть.


    1. CrazyElf
      22.05.2023 09:21

      Ну или вот вы хотите открыть и прочитать файл. Что может пойти не так? Да сразу много чего. Логично ли все эти возможные косяки проверять if-ами? Нет. Гораздо проще поймать исключение и там уже будет написана довольно локализованная проблема - такого файла нет или у вас нет прав его читать или файл лежит на сетевом диске, который отвалился, или ещё что-то. Ну и в целом да, вы никогда не знаете, какой уровень контроля окажется в итоге нужен при вызове метода. Вот обнаружили вы деление на 0 с помощью if - и что дальше то? При использовании исключений всё понятно - возникает исключение и летит наверх до того уровня, где его считают нужным поймать. И не нужно придумывать какие-то отдельные конвенции - что же вы должны делать, что возвращать, если у вас функция деления и вы обнаружили в знаменателе ноль.


      1. Satim
        22.05.2023 09:21
        -1

        Ну а что может пойти не так при работе с файлом?

        Прав нет, вызвали метод проверки прав перед открытием файла показали сообщение пользователю что прав нет.

        Файла нет, показали сообщение пользователю что файла нет.

        Код ясный и понятный, нормальные человеческие сообщения пользователю. Или вы предлагаете пользователю отдавать вот это

        "FileNotFoundError: [Errno 2] No such file or directory: 'file.txt'"

        Я предпочту написать нормально сообщение + залогировать что такой то пользователь пытался получить доступ к несуществующему файлу.

        "Файл с именем 'file.txt' не найден. Обратитесь в тех поддержку -7(999)555-55-55."


        1. CrazyElf
          22.05.2023 09:21

          Вы не в курсе, что можно ловить не общий Exception, а конкретные исключения типа того же FileNotFoundError и выдавать понятные пользователю сообщения в этом случае? Это всё-равно будет проще, чем проверять все косяки вручную, да и нет никакой гарантии, что, например, файл не удалят или не поменяют ему права между моментом, когда вы его проверите через if и моментом когда откроете файл в программе. И поэтому вам всё-равно придётся ловить исключения и как-то их обрабатывать.


          1. Satim
            22.05.2023 09:21
            -1

            В курсе. Предлагаете все конкретные исключения в except'e отлавливать?

            Я вот предпочту вообще до исключения не доводить. Ну и зачем пытаться читать файл с диска если прав нет. В том смысле что зачем пытаться выполнить код, если заранее известно что он упадет с исключением?

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


            1. CrazyElf
              22.05.2023 09:21
              +2

              То есть вы предлагаете программистам делать двойную работу? )) Сначала проверять что-то через if, а потом ещё ловить соответствующий exception. Прелестно, прелестно )) И да, хороший стиль - именно ловить в блоке try-except все конкретные exception, которые вы ожидаете, что вылетят и вы знаете, как вы хотите на них реагировать. А те, с которыми не знаете что делать - не ловить, пусть на них выше по стеку кто-то реагирует. Это в любом руководстве написано.


              1. Satim
                22.05.2023 09:21
                +1

                Вы видимо совсем не читаете мои сообщения, либо не хотите понять. Я нигде не говорил что нужно делать двойную работу. Нужно грамотно организовывать код, где-то сделать проверку if где то обработку исключения. Но не нужно подменять одно другим.


                1. CrazyElf
                  22.05.2023 09:21

                  Я всё прекрасно понимаю. У вас какой опыт вообще в программировании, сколько лет и какого грейда?
                  Я же уже приводил пример, что между проверкой на всякое и открытием файла его статус может успеть поменяться и вам всё-равно придётся проверять те исключения, которых вы пытаетесь избежать.


                  1. Satim
                    22.05.2023 09:21

                    Опыт лет 20, грейд хз на бейджике написано "Ведущий инженер-программист".


                    1. CrazyElf
                      22.05.2023 09:21

                      Ну то есть фактически senior. Ну, все за карьеру могут с разными ситуациями сталкиваться, может у вас не бывало таких задачек, где критично сделать всё идеально или около того.


            1. Vindicar
              22.05.2023 09:21
              +4

              Во-первых, даже если есть права, файл может быть занят. Или файл уже открыт, но пользователь выдернул флэшку/отключил соединение с сетевым диском. Часто бывает так, что не попытавшись выполнить операцию - не поймёшь, выполнима ли она.

              Во-вторых, в интервал времени между проверкой и открытием файла может произойти много всякого. Вон, был пример где службу обновления Steam обманывали, подсунув свой код, потому что она сначала открывала файл для проверки подписи, а потом закрывала и переоткрывала для выполнения.

              Так что от неожиданных проблем никуда не деться. Стоит ли городить отдельный огород для проблем ожидаемых, в таком случае?


            1. thevlad
              22.05.2023 09:21
              +2

              Проблема в ифами и кодами возврата, в том, что их никто не проверяет. А если и проверять тотально, то с увеличением вложенности это выливается в лапшу из ифов проверяющих коды возврата на каждом уровне.

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


              1. Satim
                22.05.2023 09:21

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


    1. whoisking
      22.05.2023 09:21
      +1

      Может тут найдётся что-то полезное LBYL vs EAFP


      1. CrazyElf
        22.05.2023 09:21

        Вот это хороший обзор двух подходов, да.


      1. Satim
        22.05.2023 09:21

        Что то подобное я и пытался сказать)


  1. 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 нужен когда в работе бизнес логики выполнена задача, которую необходимо озвучить пользователю для прозрачности процессов.


  1. one-eyed-j
    22.05.2023 09:21
    +1

    Автор, похоже, писал примеры без понимания того, как реально работают исключения. В частности в примере с открытием файла клауза finally написано просто неправильно. Если open() сгенерировала ошибку, то переменная file вообще не будет определена. В результате будет сгенерировано ещё одно исключение NameError. Уж лучше читайте учебники, чем блоггеров с Medium.


  1. BigLY
    22.05.2023 09:21
    +1

    Важно отличать подходы EAFP(Проще просить прощения, чем разрешения) и LBYL(Смотри, прежде чем прыгать). LBYL стоит применять когда ты работаешь со статическими данными, например: обработка результата функции, проверка ключа в словаре и т.д. EAFP используется когда работа ведется с динамически изменяемой средой, например: проверка доступности удаленного сервера не дает тебе гарантию, что сервер не сломается сразу после ответа на проверку, или файл не будет удален после открытия в приложении и т.д.

    Хороший пример EAFP метод raise_for_status библиотеки requests, который поднимает ошибку, а не генерирует ответ с результатом валидации статуса ответа.