Книга: Как устроен Python. Гид для разработчиков, программистов и интересующихся
Назад: 22. Субклассирование
Дальше: 24. Импортирование библиотек

23. Исключения

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

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

>>> 3/0

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

ZeroDivisionError: division by zero

Из этой трассировки следует, что в строке 1 файла <stdin> (имя «файла» интерпретатора) произошла ошибка деления на ноль. При возникновении исключения в ходе выполнения программы трассировка стека показывает, в каком файле и строке возникла проблема. Приведенный пример с интерпретатором не особенно полезен, потому что программа состоит всего из одной строки кода. Однако в больших программах возможна многоуровневая иерархия трассировки стека из-за того, что функции вызывают другие функции и методы.

Допустим, файл содержит следующий код:

def err():

1/0

 

def start():

return middle()

 

def middle():

return more()

 

def more():

err()

При попытке выполнить его вы получите следующую трассировку:

Traceback (most recent call last):

File "/tmp/err.py", line 13, in <module>

start()

File "/tmp/err.py", line 5, in start

return middle()

File "/tmp/err.py", line 8, in middle

return more()

File "/tmp/err.py", line 11, in more

err()

File "/tmp/err.py", line 2, in err

1/0

ZeroDivisionError: division by zero

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

23.1. «Посмотри, прежде чем прыгнуть»

Предположим, у вас имеется программа, которая выполняет деление. В зависимости от того, как написан код, в какой-то момент она может попытаться выполнить деление на ноль. Программисты обычно применяют два стиля обработки исключений. Первый стиль называется LBYL (Look Before You Leap, то есть «Посмотри, прежде чем прыгнуть»). Его суть в том, чтобы проверить исключительную ситуацию перед выполнением действия. В нашем случае программа проверяет делитель на ноль. Если делитель отличен от нуля, программа может выполнить деление; если нет — операцию следует пропустить.

В Python стиль LBYL можно реализовать с помощью команд if:

>>> numerator = 10

>>> divisor = 0

>>> if divisor != 0:

... result = numerator / divisor

... else:

... result = None

ПРИМЕЧАНИЕ

Принцип LBYL не дает гарантии успеха. Даже если вы проверите, что файл существует, прежде чем открывать его, это не означает, что он будет существовать потом. В многопоточных средах такая ситуация называется условием гонки (race condition).

ПРИМЕЧАНИЕ

Значение None используется для представления неопределенного состояния. Это одна из распространенных идиом мира Python. Будьте внимательны и не пытайтесь вызывать методы для переменной, которой присвоено значение None, — это приведет к выдаче исключения.

23.2. «Проще просить прощения, чем разрешения»

Другой стиль обработки исключений обычно обозначается сокращением EAFP (Easier to Ask for Forgiveness than Permission, то есть «Проще попросить прощения, чем разрешения»). Если операция завершается неудачей, исключение будет перехвачено в блоке исключения.

Конструкция try...except предоставляет механизм для перехвата исключительных ситуаций в Python:

>>> numerator = 10

>>> divisor = 0

>>> try:

... result = numerator / divisor

... except ZeroDivisionError as e:

... result = None

Конструкция try создает блок после ключевого слова try (на что указывает двоеточие и отступы). Внутри блока try находятся команды, которые могут выдавать исключения. Если при выполнении команд действительно произойдет исключение, Python ищет блок except, который перехватывает это исключение (или исключение его родительского класса).

В приведенном коде блок except перехватывает исключения, являющиеся экземплярами класса ZeroDivisionError (или его субклассов). Когда в блоке try происходит указанное исключение, выполняется блок except, а результату присваивается None.

Обратите внимание: в строке

except ZeroDivisionError as e:

в последней позиции стоит двоеточие. Эта часть не обязательна: если она присутствует, то e (или другое имя переменной, которое вы выбрали) будет указывать на экземпляр исключения ZeroDivisionError. Вы можете проанализировать объект исключения, нередко в нем содержится более подробная информация. Переменная e указывает на активное исключение. Если вы не включите секцию as e в конце команды except, активное исключение в программе все равно будет, но вы не сможете обратиться к его экземпляру.

СОВЕТ

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

Так как стиль LBYL не гарантирует успешного предотвращения ошибок, обычно разработчики Python предпочитают стиль EAFP. Несколько практических правил обработки исключений:

• Обрабатывайте те ошибки, которые вы можете обработать и которые можно ожидать в программе.

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

• Используйте глобальный обработчик исключений для корректной обработки непредвиденных ошибок.

СОВЕТ

Если вы пишете серверное приложение, которое должно работать непрерывно, один из возможных способов показан ниже (функции process_input и log_error не существуют и приведены исключительно в демонстрационных целях):

while 1:

try:

result = process_input()

except Exception as e:

log_error(e)

 

23.3. Несколько возможных исключений

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

try:

some_function()

except ZeroDivisionError as e:

# Обработка конкретного исключения

except Exception as e:

# Обработка других исключений

В этом примере, когда some_function выдает исключение, интерпретатор сначала проверяет, соответствует ли оно ошибке класса ZeroDivisionError или его субкласса. Если условие не выполняется, код проверяет, относится ли исключение к субклассу Exception. После входа в блок except Python уже не проверяет последующие блоки.

Если исключение не обработано цепочкой, оно должно быть обработано кодом где-то в стеке вызовов. Если исключение так и остается необработанным, Python прекращает выполнение и выводит трассировку стека. Пример обработки нескольких исключений встречается в стандартной библиотеке. Модуль argparse из стандартной библиотеки предоставляет простой механизм разбора параметров командной строки. Он позволяет указать тип некоторых параметров — например, целых чисел или файлов (все параметры поступают в строковой форме). В методе ._get_value встречаются примеры использования нескольких секций except. В зависимости от типа инициированного исключения выдаются разные сообщения об ошибках:

def _get_value(self, action, arg_string):

 

type_func = self._registry_get('type', action.type, action.type)

if not callable(type_func):

msg = _('%r is not callable')

raise ArgumentError(action, msg % type_func)

 

# преобразование значения к соответствующему типу

try:

result = type_func(arg_string)

 

# ArgumentTypeError - признак ошибки

except ArgumentTypeError:

name = getattr(action.type, '__name__', repr(action.type))

msg = str(_sys.exc_info()[1])

raise ArgumentError(action, msg)

 

# TypeError и ValueErrors тоже являются признаками ошибок

except (TypeError, ValueError):

name = getattr(action.type, '__name__', repr(action.type))

args = {'type': name, 'value': arg_string}

msg = _('invalid %(type)s value: %(value)r')

raise ArgumentError(action, msg % args)

 

# возвращается преобразованное значение

return result

ПРИМЕЧАНИЕ

Этот пример показывает, что одна команда except может перехватывать сразу несколько типов исключений, для чего следует передать кортеж классов исключений:

except (TypeError, ValueError):

 

ПРИМЕЧАНИЕ

Этот пример также демонстрирует старый стиль форматирования строк с использованием оператора %. Строки

msg = _('invalid %(type)s value: %(value)r')

raise ArgumentError(action, msg % args)

в современном стиле записываются так:

msg = _('invalid {type!s} value: {value!r}')

raise ArgumentError(action, msg.format(**args))

 

23.4. finally

Еще одна конструкция обработки ошибок — секция finally. Эта команда используется для определения кода, который будет выполняться всегда — независимо от того, возникло исключение или нет.

Блок finally выполняется всегда. Если исключение было обработано, то блок finally будет выполнен после обработки. Если исключение не было обработано, то блок finally выполняется, а исключение инициируется заново:

try:

some_function()

except Exception as e:

# Обработка ошибок

finally:

# Завершающие действия

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

Пример из модуля timeit, входящего в стандартную библиотеку, помогает понять полезность команды finally. Модуль timeit позволяет разработчику проводить хронометраж кода. В частности, во время проведения хронометража модуль приказывает уборщику мусора прекратить работу. Однако после завершения хронометража уборщик мусора должен быть снова включен независимо от того, был ли хронометраж завершен успешно или произошла ошибка.

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

def timeit(self, number=default_number):

"""Хронометраж 'number' выполнений основной команды.

 

Для повышения точности команда подготовки выполняется однократно,

после чего время, необходимое для многократного выполнения основной

команды, возвращается как вещественное число (в секундах). Аргумент

определяет количество выполнений цикла (по умолчанию один миллион).

Основная команда, команда подготовки и используемая функция таймера

передаются конструктору.

"""

it = itertools.repeat(None, number)

gcold = gc.isenabled()

gc.disable()

try:

timing = self.inner(it, self.timer)

finally:

if gcold:

gc.enable()

return timing

При вызове self.inner может произойти исключение, но, поскольку стандартная библиотека использует finally, уборка мусора всегда будет включаться независимо от исключения (если логическая переменная gcold истинна).

ПРИМЕЧАНИЕ

В этой книге менеджеры контекста не рассматриваются, но, чтобы подготовить вас к будущей карьере эксперта Python, мы приведем небольшой совет. Комбинация try/finally в Python считается кодом «с душком». Опытные программисты Python в таких случаях применяют менеджер контекста. Включите эту тему в список вопросов, которые вам будет нужно изучить после освоения базовых возможностей Python.

23.5. Секция else

Необязательная секция else в команде try выполняется в том случае, если не было выдано никаких исключений. Она должна следовать за всеми секциями except и выполняется перед блоком finally. Простой пример:

>>> try:

... print('hi')

... except Exception as e:

... print('Error')

... else:

... print('Success')

... finally:

... print('at last')

hi

Success

at last

Ниже приведен пример из модуля heapq стандартной библиотеки. Как следует из комментариев, существует ускоренное решение, если число запрашиваемых значений превышает размер кучи. Тем не менее, если при попытке получения размера кучи произойдет ошибка, в коде вызывается pass. В результате ошибка игнорируется, а выполнение продолжается по более медленному варианту. Если ошибки не было, можно пойти по пути else и выбрать быстрый путь, если n превышает размер кучи:

def nsmallest(n, iterable, key=None):

# ....

 

# Если n>=size, быстрее использовать sorted()

try:

size = len(iterable)

except (TypeError, AttributeError) as e:

pass

else:

if n >= size:

return sorted(iterable, key=key)[:n]

# Часть кода пропущена .... Использовать более медленный способ

23.6. Выдача исключений

Помимо перехвата исключений, Python также дает возможность выдавать исключения в коде (то есть инициировать их). Вспомните, что Дзен Python рекомендует явно выражать свои намерения и бороться с искушением что-либо предполагать. Если функции передается неверный ввод и вы знаете, что не сможете с ним справиться, можно выдать исключение. Исключения являются субклассами класса BaseException, а для их выдачи используется команда raise:

raise BaseException('Program failed')

Обычно в программе выдается не обобщенное исключение класса BaseException, а исключение одного из его субклассов — уже готовых или определенных разработчиком. В другом распространенном варианте команда raise используется сама по себе. Вспомните, что внутри команды except существует так называемое активное исключение. В таком случае можно обойтись минимальной командой raise. Эта команда позволяет обработать исключение, а потом заново выдать исходное исключение. При попытке выполнения кода

except (TypeError, AttributeError) as e:

log('Hit an exception')

raise e

вам удастся успешно инициировать исходное исключение, но трассировка стека будет показывать, что исходное исключение теперь произошло в строке с командой raise e, а не там, где оно произошло сначала. У проблемы есть два решения. Первое — использование минимальной команды raise. Второе — использование сцепленных исключений (см. далее).

Рассмотрим пример из модуля configparser стандартной библиотеки. Этот модуль обеспечивает чтение и создание INI-файлов. INI-файлы обычно используются для настройки конфигурации; они были по­пулярны до появления форматов JSON и YAML. Метод .read_dict пытается прочитать конфигурацию из словаря. Если экземпляр находится в «жестком» режиме, при попытке добавления одной и той же секции более одного раза произойдет исключение. Если «жесткий» режим не включен, метод допускает наличие дубликатов ключей; используется последний ключ. Часть метода, демонстрирующая вариант с минимальной командой raise:

def read_dict(self, dictionary, source='<dict>'):

elements_added = set()

for section, keys in dictionary.items():

section = str(section)

try:

self.add_section(section)

except (DuplicateSectionError, ValueError):

if self._strict and section in elements_added:

raise

elements_added.add(section)

# Часть кода пропущена ....

Если дубликат добавляется в «жестком» режиме, трассировка стека покажет ошибку в методе .add_section, потому что она произошла именно там.

23.7. Упаковка исключений

В Python 3 появилась новая возможность, сходная с минимальной командой raise; она описана в «PEP 3134 — Exception Chaining and Embedded Tracebacks». При обработке исключения в коде обработки может произойти другое исключение. В подобных ситуациях бывает полезно знать об обоих исключениях.

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

>>> def log(msg):

... raise SystemError("Logging not up")

 

>>> def divide_work(x, y):

... try:

... return x/y

... except ZeroDivisionError as ex:

... log("System is down")

Если при вызове divide_work передать 5 и 0, Python выведет информацию о двух ошибках, ZeroDivisionError и SystemError. Ошибка SystemError будет выведена последней, потому что она произошла последней:

>>> divide_work(5, 0)

Traceback (most recent call last):

File "begpy.py", line 3, in divide_work

return x/y

ZeroDivisionError: division by zero

 

During handling of the above exception, another exception occurred:

 

Traceback (most recent call last):

File "begpy.py", line 1, in <module>

divide_work(5, 0)

File "begpy.py", line 5, in divide_work

log("System is down")

File "begpy.py", line 2, in log

raise SystemError("Logging not up")

SystemError: Logging not up

Предположим, облачный сервис журнала заработал (функция log уже не выдает ошибку). Если вы хотите изменить тип ZeroDivisionError в divide_work на ArithmeticError, используйте синтаксис, описанный в PEP 3134. Для этого можно воспользоваться синтаксисом raise... from:

>>> def log(msg):

... print(msg)

 

>>> def divide_work(x, y):

... try:

... return x/y

... except ZeroDivisionError as ex:

... log("System is down")

... raise ArithmeticError() from ex

Теперь вы видите два исключения: исходное ZeroDivisionError и исключение ArithmeticError, которое уже не скрывается ZeroDivisionError:

>>> divide_work(3, 0)

Traceback (most recent call last):

File "begpy.py", line 3, in divide_work

return x/y

ZeroDivisionError: division by zero

Во время обработки вышеуказанного исключения произошло другое исключение:

Traceback (most recent call last):

File "begpy.py", line 1, in <module>

divide_work(3, 0)

File "begpy.py", line 6, in divide_work

raise ArithmeticError() from ex

ArithmeticError

Если вы хотите подавить исходное исключение ZeroDivisionError, используйте следующий код (см. «PEP 0409 — Suppressing exception context»):

>>> def divide_work(x, y):

... try:

... return x/y

... except ZeroDivisionError as ex:

... log("System is down")

... raise ArithmeticError() from None

Теперь видна только внешняя ошибка ArithmeticError:

>>> divide_work(3, 0)

Traceback (most recent call last):

File "begpy.py", line 1, in <module>

divide_work(3, 0)

File "begpy.py", line 6, in divide_work

raise ArithmeticError() from None

ArithmeticError

23.8. Определение собственных исключений

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

BaseException

SystemExit

KeyboardInterrupt

GeneratorExit

Exception

StopIteration

ArithmeticError

FloatingPointError

OverflowError

ZeroDivisionError

AssertionError

AttributeError

BufferError

EnvironmentError

IOError

OSError

EOFError

ImportError

LookupError

IndexError

KeyError

MemoryError

NameError

UnboundLocalError

ReferenceError

RuntimeError

NotImplementedError

SyntaxError

IndentationError

TabError

SystemError

TypeError

ValueError

UnicodeError

UnicodeDecodeError

UnicodeEncodeError

UnicodeTranslateError

Warning

DeprecationWarning

PendingDeprecationWarning

RuntimeWarning

SyntaxWarning

UserWarning

FutureWarning

ImportWarning

UnicodeWarning

BytesWarning

Для определения собственных исключений субклассируйте класс Exception или один из его субклассов. Дело в том, что все остальные субклассы BaseException не обязательно являются «исключениями». Например, если вы перехватываете KeyboardInterrupt, вам не удастся прервать выполнение процесса клавишами Ctrl+C. Если вы перехватите GeneratorExit, перестанут работать генераторы.

Вот как выглядит исключение для определения нехватки информации в программе:

>>> class DataError(Exception):

... def __init__(self, missing):

... self.missing = missing

Использовать нестандартное исключение достаточно просто:

>>> if 'important_data' not in config:

... raise DataError('important_data missing')

23.9. Итоги

В этой главе были представлены стратегии обработки исключений. В стратегии LBYL перед выполнением операции вы проверяете, что в текущей ситуации не будет выдана ошибка. В стратегии EAFP весь код, который может выдать ошибку, заключается в блок try/catch. В Python предпочтение отдается второму стилю программирования.

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

23.10. Упражнения

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

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

Назад: 22. Субклассирование
Дальше: 24. Импортирование библиотек