Использование исключений. Часть 1

Многие программистов убеждены, что исключения должны использоваться только в исключительных случаях. И они не правы. В сообществе Python убеждены, что использование исключений приводит к более чистому коду, который легче читается. Причем без огромных затрат в производительности, с которыми обычно ассоциируются исключения в других языках.

Использование исключений для написания более чистого кода?

Когда я говорю об “использовании исключений”, я специально не упоминаю о создании какой-то сумасшедшей иерархии исключений в вашем пакете и вызове их при каждом удобном случае. Это обычно приводит к сложности понимания кода и его поддержки. Данный момент широко обсуждался и хорошо описан в блоге Джоела Спольски.

Примечание. Python позволяет в большей мере избежать споров “коды ошибок vs исключения”. Вопрос возможности возврата из функции множественных значений и возможность возврата из функции значений различных типов (т.е. Возврата None или что-то похожего в случае ошибки) остается спорным. Но это находиться вне данной темы.

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

Отступление: Как работает цикл for

Каждый раз, когда вы используете цикл for для прохода по перечислению (обычно это все виды последовательностей и объекты с определенными методами __item__() или __getitem__()), он должен как-то определять когда нужно остановить итерации. Давайте посмотрим на код:

words = ['exceptions', 'are', 'useful']
for word in words:
    print(word)

Как цикл for знает, что достиг последнего элемента в words и должен прекратить попытки получить следующее значение ? Ответ может вас удивить: список вызывает исключение StopIteration.

В действительности все перечисления используют данный механизм. Когда for выполняет первую итерицию, он вызывает метод iter() на объекте. Метод создает iterator для объекта, способный вернуть содержимое объекта в последовательности. Для успешного вызова метода iter(), объект должен реализовывать интерфейс (протокол) итераций (должен быть определен метод __iter__()) или интерфейс (протокол) последовательностей (определен метод __getitem__()).

Как видно, оба метода __item__() и __getitem__() должны вызывать исключение, когда элементы, по которым проходит цикл, заканчиваются. __iter__() вызывает исключение StopIteration, как описано выше, а __getitem__() вызывает исключение IndexError. Вот как цикл for знает, что нужно остановиться.

Вывод: Если вы где-то в своем коде используете цикл for, то вы используете и исключения

LBYL vs. EAFP

Это все так и хорошо, что исключения широко используются в основных конструкциях Python, но другой вопрос почему. Ко всему прочему, цикл for можно было написать и без использования исключений для маркировки конца последовательности. Действительно, использование исключений можно было и избежать.

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

def print_object(some_object):
    # check if the object is printable...
    if isinstance(some_object, str):
        print(some_object)
    elif isinstance(some_object, dict):
        print(some_object)
    elif isinstance(some_object, list):
        print(some_object)
    # 97 elifs later...
    else:
        print("unprintable object")

Это тривиальная функция для вызова print() для объекта. Если объект не может быть выведен, то должно быть напечатано сообщение об ошибке.

Попытка заранее предвидеть все возможные ошибочные ситуации заранее обречена на провал (и это ужасно выглядит). Утиная типизация - центральная особенность Python, но эта функция некорректно напечатает сообщение об ошибки для объекта, который может быть распечатан, но не был корректно проверен.

Эта функция может быть переписана:

def print_object(some_object):
    # check if the object is printable...
    try:
        printable = str(some_object)
        print(printable)
    except TypeError:
        print("unprintable object")

Если объект может быть приведен к строке, то это выполняется и объект распечатывается. Если попытка вызывает исключение, то печатается ошибка. Та же идея, то более простая в реализации (строки в блоке try можно было объединить, но так пример более нагляден). Так же заметьте, что мы проверяем именно на исключение TypeError, которое вызывается, если приведение не возможно. Никогда не используйте общий except: или вы будете подавлять ошибки, о которых и не подозреваете.

Но подождите, это еще не все!

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

В переписанной ниже версии функции print_object() оператор else выполняется только в случае, когда блок try не вызвал исключения. Это концептуально схоже с использованием оператора else в цикле for (это само по себе удобная, но мало известная конструкция). Так же в новой версии исправлена ошибка: мы ловили исключение TypeError, подразумевая, что только преобразование str() может его вызвать. Но что, если оно будет (как-то) вызвано в print() и не иметь ничего общего с преобразованием в строку ?

def print_object(some_object):
    # check if the object is printable
    try:
        printable = str(some_object)
    except TypeError:
        print("unprintable object")
    else:
        print(printable)

Сейчас строка print() будет вызвана только, если не будет ни какого исключения. Если print() вызовет исключение, то оно поднимется по стеку вверх, как обычно. Оператор else часто упускается при обработке исключений, но он удобен в подобных ситуациях. Другое использование оператора else, когда код в блоке try требует после себя действий очистки, как в следующем примере:

def display_username(user_id):
    try:
        db_connection = get_db_connection()
    except DatabaseEatenByGrueError:
        print('Sorry! Database was eaten by a grue.')
    else:
        print(db_connection.get_username(user_id))
        db_connection.cleanup()

Источник: Write Cleaner Python: Use Exceptions

 
comments powered by Disqus