Книга: Марк Лутц - Изучаем Python, 5-е изд., Т. 2
Назад: Основы исключений
Дальше: Объекты исключений

Детали обработки исключений

 

В предыдущей главе мы кратко взглянули на связанные с исключениями операторы в действии. Здесь мы собираемся копнуть глубже — в этой главе предоставляется более формальное введение в синтаксис Python для обработки исключений. В частности, мы исследуем детали, лежащие в основе операторов try, raise, assert и with. Как вы увидите, несмотря на то, что указанные операторы в основном прямолинейны, они предлагают мощные инструменты для работы с исключительными условиями в коде на Python.
г , Одно заблаговременное процедурное примечание. В последние годы история об исключениях значительным образом менялась. Начиная с Python
| 2.5, конструкция finally может появляться в операторе try вместе с
конструкциями except и else (ранее комбинировать их было невозможно). Кроме того, в версиях Python 3.0 и Python 2.6 новый оператор диспетчера контекста with стал официальным, и теперь определяемые пользователем исключения должны быть реализованы в виде экземпляров классов, унаследованных от встроенного суперкласса исключения. Вдобавок Python
З.Х имеет слегка модифицированный синтаксис для оператора raise и конструкций except, часть которого доступна в Python 2.6 и 2.7.
Основное внимание в настоящем издании я буду уделять состоянию исключений в последних выпусках Python 2.Х и З.Х. Но так как вполне вероятно, что в течение какого-то времени вы по-прежнему будете встречать в коде первоначальные методики, попутно я буду указывать, каким образом все развивалось в данной области.
Оператор try/except/else
После ознакомления с основами самое время переходить к деталям. В последующем обсуждении я сначала представлю try/except/else и try/finally как отдельные операторы, поскольку в версиях, предшествующих Python 2.5, они исполняют разные роли и не могут комбинироваться, а в наши дни отличаются, во всяком случае, логически. Согласно предыдущей врезке “На заметку!” в Python 2.5 и более поздних версиях except и finally могут смешиваться в одном операторе try; последствия такого объединения мы увидим после исследования двух первоначальных форм обособленно.
Синтаксически try представляет собой составной оператор, содержащий несколько частей. Он начинается со строки заголовка try, за которой следует блок операторов (обычно) с отступами, затем один или больше конструкций except, идентифицирующих перехватываемые исключения вместе с блоками для их обработки, и в конце необязательная конструкция else с блоком кода. Слова try, except и else связываются друг с другом за счет их отступа на одинаковый уровень (т.е. выравнивания по вертикали). Для справки ниже показан общий и наиболее полный формат в Python З.Х:
try:
операторы # Главное действие, выполняемое первым
except имя1:
операторы # Выполняются, если в течение блока try
# сгенерировалось исключение имя1
except (имя2, имяЗ):
операторы # Выполняются, если произошло любое
# из указанных исключений
except имя4 as переменная:
операторы # Выполняются, если сгенерировалось исключение имя4,
# экземпляр исключения присваивается переменной
except:
операторы # Выполняются, если были сгенерированы
# все остальные исключения
else:
операторы # Выполняются, если в течение блока try исключения
# не генерировались
Семантически блок под заголовком try в этом операторе представляет главное действие оператора — код, который вы пытаетесь выполнить и помещаете его внутрь логики обработки ошибок. Конструкции except определяют обработчики для исключений, генерируемых в течение блока try, а конструкция else (если есть) предоставляет обработчик, подлежащий выполнению, если никакие исключения не возникали. Элемент переменная относится к характерным особенностям операторов raise и классов исключений, которые мы обсудим более подробно позже в главе.
Как работают операторы try
С точки зрения работы ниже описано, как выполняются операторы try. При входе в оператор try интерпретатор Python запоминает текущий контекст программы, чтобы он мог возвратиться к нему, если возникнет исключение. Первыми выполняются операторы, вложенные внутрь заголовка try. То, что происходит следующим, зависит от того, генерировались ли исключения во время выполнения операторов блока try, и соответствуют ли они тем, которые отслеживает try.
• Если исключение происходит во время выполнения операторов блока try и оно соответствует одному из перечисленных в операторе, тогда интерпретатор Python переходит обратно на try и запускает операторы под первой конструкцией except, дающей совпадение со сгенерированным исключением. Затем объект сгенерированного исключения присваивается переменной, указанной после ключевого слова as в конструкции (при его наличии). После выполнения блока except поток управления возобновляется ниже полного оператора try (если только сам блок except не сгенерирует еще одно исключение, в случае чего процесс начинается заново с этой точки в коде).
• Если исключение происходит во время выполнения операторов блока try, но оно не соответствует одному из перечисленных в операторе, тогда исключение распространяется до следующего самого последнего введенного оператора try, который дает совпадение с исключением. Если найти такой оператор try не удается и поиск достигает верхнего уровня процесса, то интерпретатор Python уничтожает программу и выводит стандартное сообщение об ошибке.
• Если во время выполнения операторов блока try никаких исключений не произошло, тогда интерпретатор Python запускает операторы под строкой else (при ее наличии) и поток управления возобновляется с места, которое находится ниже полного оператора try.
Другими словами, конструкции except перехватывают любые совпадающие исключения, которые происходят во время выполнения блока try, а конструкция else запускается, только когда при выполнении блока try исключения не возникают. Сгенерированные исключения сопоставляются с исключениями, перечисленными в конструкциях except, по отношениям с суперклассами, которые мы исследуем в следующей главе, и пустая конструкция except (без имен исключений) соответствует всем (или всем остальным) исключениям.
Конструкции except являются специализированными обработчиками исключений — они перехватывают исключения, которые возникают только в рамках операторов ассоциированного блока try. Однако так как операторы блока try иногда вызывают функции, реализованные где-то в другом месте программы, источник исключения может оказаться вне самого оператора try.
Фактически блок try способен обращаться к произвольно крупным объемам программного кода, в том числе кода, содержащего операторы try — при возникновении исключений поиск в таких операторах будет осуществляться первым. То есть операторы try могут вкладываться во время выполнения; в главе 36 мы рассмотрим эту тему подробнее.
Конструкции оператора try
Когда вы пишете оператор try, после заголовка try могут появляться разнообразные конструкции. В табл. 34.1 приведена сводка по всем возможным формам — вы обязаны использовать хотя бы одну. Некоторые из них мы уже встречали: как известно, конструкции except перехватывают исключения, конструкции finally запускаются при выходе, а конструкции else выполняются, если исключения не возникали.
Таблица 34.1. Формы конструкций оператора try
Форма конструкцииИнтерпретация
except:Перехватывает все (или все остальные) типы исключений
except имя:Перехватывает только специфическое исключение
except имя as значение:Перехватывает указанное исключение и присваивает его экземпляр
except (имя!, имя2) :Перехватывает любое из перечисленных исключений
except {имя1, имя2) as значение:Перехватывает любое из перечисленных исключений и присваивает его экземпляр
else:Выполняется, если в блоке try исключения не генерировались
finally:Всегда выполняется при выходе
Формально может присутствовать любое количество конструкций except, но else записывается, только если есть, по крайней мере, одна конструкция except, и допускается только одна else и одна finally. До версии Python 2.4 конструкция finally должна быть одна (без else или except); в действительности try/finally — другой оператор. Тем не менее, начиная с Python 2.5, конструкция finally может появляться в операторе, где есть except и else (правила упорядочения будут более подробно обсуждаться позже в главе, когда мы займемся унифицированным оператором try).
Мы будем исследовать элементы с дополнительной частью as значение при рассмотрении оператора raise далее в главе. Они предоставляют доступ к объектам, которые были сгенерированы как исключения.
Перехват любого и всех исключений
Новыми для нас являются первая и четвертая строки в табл. 34.1.
• Конструкции except, не содержащие имен исключений (except:), перехватывают все исключения, которые до того не перечислялись в операторе try.
• Конструкции except, в которых указан список исключений в круглых скобках (except (el, е2, еЗ) :), перехватывают любое из перечисленных исключений.
Поскольку интерпретатор Python ищет совпадение внутри заданного try путем инспектирования конструкций except сверху вниз, версия в круглых скобках имеет такой же эффект, как указание каждого исключения в собственной конструкции except, но блок операторов, ассоциированный с каждым исключением, придется писать только раз. Ниже приведен пример множества конструкций except в работе, который демонстрирует, насколько специфичными могут оказаться ваши обработчики:
try:
action()
except NameError:
except IndexError:
except KeyError:
except (AttributeError, TypeError, SyntaxError):
else:
Если во время выполнения вызова функции action генерируется исключение, тогда интерпретатор Python возвращается к try и ищет первую конструкцию except, в которой указано имя возникшего исключения. Он инспектирует конструкции except сверху вниз плюс слева направо и запускает операторы под первой конструкцией, давшей совпадение. Если ни одна конструкция не обеспечила совпадение, тогда исключение распространяется после этого оператора try. Обратите внимание, что конструкция else выполняется, только когда в action никаких исключений не произошло — она не запускается в случае генерации исключения, для которого отсутствует соответствующая конструкция except.
Перехват всех исключений: пустая конструкция except и Exception
Если вам действительно нужна “всеобъемлющая” конструкция, то подойдет пустая конструкция except:
try:
action() except NameError:
... # Обработка NameError
except IndexError:
... # Обработка IndexError
except:
... # Обработка всех остальных исключений
else:
... # Обработка случая отсутствия исключений
Пустая конструкция except представляет собой своеобразное групповое средство — по причине перехвата всего она позволяет вашим обработчикам быть настолько универсальными или специфическими, насколько вы хотите. В некоторых сценариях такая форма может оказаться более удобной, чем перечисление всех возможных исключений в try. Скажем, следующий оператор try перехватывает все, ничего не перечисляя:
try:
action() except:
... # Перехват всех возможных исключений
Однако пустые конструкции except также привносят ряд вопросов при проектировании. Несмотря на удобство, они могут перехватывать непредвиденные системные исключения, не относящиеся к вашему коду, и неумышленно перехватывать исключения, которые предназначены для другого обработчика. Например, даже системные вызовы для выхода и нажатия комбинаций клавиш <Ctrl+C> генерируют в Python исключения, которые обычно желательно пропускать. Хуже того, пустые конструкции except способны также отлавливать подлинные программные ошибки, для которых вероятно имеет смысл видеть соответствующие сообщения. Мы вернемся к этому вопросу при рассмотрении затруднений в конце данной части книги. Сейчас я просто рекомендую применять их с осторожностью.
В Python З.Х более строго поддерживается альтернатива, которая решает одну из таких проблем — перехват исключения по имени Exception дает почти тот же самый результат, что и пустая конструкция except, но игнорирует исключения, связанные с системными вызовами для выхода:
try:
action() except Exception:
... # Перехват всех возможных исключений кроме вызовов для
выхода
Мы исследуем, как эта форма делает свою магию, в следующей главе, когда приступим к изучению классов исключений. Выражаясь кратко, она работает из-за того, что исключения дают совпадение, если являются подклассами класса, указанного в конструкции except, a Exception представляет собой суперкласс для всех исключений, которые вы должны перехватывать подобным образом. Данная форма обеспечивает почти такое же удобство пустой конструкции except без риска перехвата событий выхода. Несмотря на то что она лучше, с ней связаны те же самые опасности — особенно в плане маскирования программных ошибок.
Примечание, касающееся нестыковки версий. Дополнительные сведения о порции as конструкций except в try также ищите далее при описании оператора raise. Синтаксически Python З.Х требует формы конструкции обработчика except Е as V:, перечисленной в табл. 34.1 и используемой в книге, а не более старой формы except Е, V:. Последняя форма все еще доступна (но не рекомендуется) в Python 2.6 и 2.7: если она применяется, то преобразуется в первую форму.
Изменение было внесено с целью устранения путаницы касательно двойной роли запятых в более старой форме. В этой форме два альтернативных исключения прекрасно записываются как except (El, Е2) Поскольку Python З.Х поддерживает только форму as, запятые в конструкции обработчика всегда означают кортеж независимо от того, используются круглые скобки или нет, а значения интерпретируются как альтернативные исключения, подлежащие перехвату.
Тем не менее, как будет показано далее, такой вариант не изменяет правила областей видимости в Python 2.Х: даже с новым синтаксисом as в Python 2.Х переменная V по-прежнему доступна после блока except. В Python З.Х переменная V позже не будет доступной и на самом деле принудительно удаляется.
Конструкция else оператора try
Целевое назначение конструкции else не всегда сразу очевидно для новичков в Python. Однако без нее отсутствует прямой способ сообщить (без установки и проверки булевских флагов), продолжил поток управления выполнение после оператора try из-за того, что никаких исключений не возникало или же исключение произошло и обработано. В любом случае мы оказываемся после оператора try:
try:
...выполнить код...
except IndexError:
...обработать исключение...
# Мы сюда попали из-за того, что try потерпел неудачу или же прошел?
Во многом подобно тому, как конструкции else в циклах придают причине выхода большую очевидность, конструкция else предоставляет в операторе try синтаксис, который делает то, что произошло, ясным и недвусмысленным:
try:
...выполнить код...
except IndexError:
...обработать исключение...
else:
...исключения не возникали...
Вы можете почти полностью с моделировать конструкцию else, переместив ее код в блок try:
try:
...выполнить код...
...исключения не возникали...
except IndexError:
...обработать исключение...
Тем не менее, такой прием может привести к некорректной классификации исключения. Если действие “исключения не возникали” сгенерирует экземпляр IndexError, то он будет зарегистрирован как отказ блока try и ошибочно запустит обработчик исключений ниже try (тонко, но верно!). За счет применения взамен явной конструкции else вы делаете логику более очевидной и гарантируете, что обработчики except будут запускаться только для реальных отказов в коде, помещенном внутрь try, а не для отказов в действии для случая “исключения не возникали” конструкции else.
Пример: стандартное поведение
Поскольку поток управления программы легче объяснять с помощью Python, чем на естественном языке, давайте рассмотрим несколько примеров, которые дополнительно проиллюстрируют основы исключений в контексте более крупных фрагментов кода из файлов.
Я уже упоминал, что исключения, не перехваченные операторами try, проникают на верхний уровень процесса Python и выполняют стандартную логику обработки исключений Python (т.е. интерпретатор Python прекращает работу функционирующей программы и выводит стандартное сообщение об ошибке). В целях демонстрации запуск следующего файла модуля bad.py приводит к генерированию исключения, связанного с делением на ноль:
def gobad(х, у): return х / у
def gosouth(х):
print(gobad(х, 0))
gosouth(1)
Так как программа игнорирует инициируемое ею исключение, интерпретатор Python уничтожает ее и выводит сообщение:
% python bad.py
Traceback (most recent call last):
File MC:\Code\bad.py", line 7, in <module> gosouth(1)
File "C:\Code\bad.py", line 5, in gosouth print(gobad(x, 0))
File "C:\Code\bad.py", line 2, in gobad return x / у
ZeroDivisionError: division by zero
Трассировка (самый последний вызов указан последним) :
Файл "C:\Code\bad.py", строка 1, в <модуль> gosouth (1)
Файл "С:\Code\bad.py”, строка 5, в gosouth print (gobad (х, 0))
Файл "С: \Code\bad.py", строка 2, в gobad return х / у
Ошибка деления на ноль: деление на ноль
Файл модуля bad.py выполнялся в окне командной оболочки Python З.Х. Сообщение состоит из трассировки стека (Traceback) и имени сгенерированного исключения вместе с сопутствующими деталями. В трассировке стека перечислены все строки, которые были активными, когда возникло исключение, от старых к новым. Обратите внимание, что поскольку мы работаем не в интерактивном сеансе, в данном случае информация об имени файла и номере строки более полезна. Скажем, здесь мы видим, что деление на ноль произошло в последней записи трассировки — в строке 2 файла bad.py, т.е. операторе return 1.
1 Как упоминалось в предыдущей главе, текст сообщений об ошибках и трассировок стека имеет тенденцию варьироваться с течением времени и в зависимости от оболочки. Не беспокойтесь, если ваши сообщения об ошибках не полностью совпадают с приведенными в книге. Например, когда пример запускается в оболочке IDLE из Python 3.7, текст сообщения об ошибках содержит имена файлов с полными абсолютными путями.
Поскольку Python обнаруживает и сообщает обо всех ошибках во время выполнения за счет генерирования исключений, исключения тесно связаны с идеями обработки ошибок и отладки в целом. Если вы прорабатывали рассматриваемые в книге примеры, тогда в ходе дела, несомненно, сталкивались с одним или двумя исключениями — даже опечатки обычно генерируют SyntaxError или другое исключение, когда файл импортируется или выполняется (т.е. при запуске компилятора). По умолчанию вы получаете полезное отображение ошибки, подобное только что показанному, которое помогает отследить проблему.
Часто такое стандартное сообщение об ошибке — это все, что вам нужно для устранения проблем в коде. Для решения более сложных задач отладки вы можете перехватывать исключения посредством операторов try либо использовать один из инструментов отладки, которые были представлены в главе 3 первого тома и будут подытожены в главе 36, например, стандартный библиотечный модуль pdb.
Пример: перехват встроенных исключений
Стандартная обработка исключений нередко оказывается именно тем, что вас интересует — особенно в случае кода из файла сценария верхнего уровня, где ошибка зачастую должна немедленно прекращать работу программы. Для многих программ предпринимать в коде что-то более специфичное касательно ошибок нет никакой необходимости.
Однако иногда вам захочется перехватывать ошибки и восстанавливаться после них. Если нежелательно, чтобы ваша программа прекращала работу, когда интерпретатор Python генерирует исключение, тогда просто перехватите ее, поместив программную логику внутрь оператора try. Это важная возможность для таких программ, как сетевые серверы, которые обязаны функционировать постоянно. Скажем, в следующем коде из файла kaboom.py производится перехват и восстановление после ошибки TypeError, генерируемой интерпретатором Python сразу же после того, как вы попытаетесь выполнить конкатенацию списка и строки (вспомните, что операция + ожидает с обеих сторон последовательности того же самого типа):
def kaboom(x, у):
print (х + у) # Генерируется TypeError
try:
kaboom([0, 1, 2], 'spam')
except TypeError: # Перехват и восстановление
print('Hello world!’)
print('resuming here’) # Продолжение здесь независимо от того,
# было исключение или нет
Когда в функции kaboom возникает исключение, поток управления переходит на конструкцию except оператора try, которая выводит сообщение. Поскольку после такого перехвата исключение становится “недействующим”, программа продолжает выполнение ниже try, а не прекращается интерпретатором Python. В сущности, код обрабатывает и очищает ошибку, восстанавливая ваш сценарий:
% python kaboom.py
Hello world!
resuming here
Имейте в виду, что как только ошибка перехвачена, поток управления возобновляет выполнение с места, где вы ее перехватили (т.е. после try); не существует прямого способа перейти обратно к тому месту, где исключение возникло (здесь в функции kaboom). В известном смысле исключения больше похожи на простые переходы, чем на вызовы функций — возвратиться в код, который сгенерировал ошибку, возможности нет.
Оператор try/finally
Другая разновидность оператора try является специализацией, имеющей отношение к действиям финализации (она же завершение). Если в состав try входит конструкция finally, тогда интерпретатор Python будет всегда запускать ее блок операторов “при выходе” из оператора try независимо от того, возникало исключение во время выполнения блока try или нет. Вот общая форма:
try:
операторы # Это действие выполняется первым
finally:
операторы # Этот код всегда выполняется при выходе
В этом варианте интерпретатор Python начинает с запуска блока операторов, ассоциированных с заголовком try, как обычно. То, что происходит следующим, зависит от того, возникало ли исключение во время выполнения блока try.
• Если во время выполнения блока try исключение не возникало, тогда интерпретатор Python переходит к выполнению блока finally и продолжает выполнение с места после оператора try.
• Если во время выполнения блока try возникло исключение, то Python по-прежнему выполняет блок finally, но затем исключение распространяется до ранее пройденного оператора try или до стандартного обработчика исключений; программа не возобновляет выполнение с места ниже конструкции finally оператора try. То есть блок finally запускается, даже если генерируется исключение, но в отличие от except конструкция finally не останавливает исключение — после выполнения блока finally оно продолжает быть сгенерированным.
Форма try/finally удобна, когда вы хотите иметь полную уверенность в том, что действие произойдет после выполнения определенного кода независимо от поведения программы, касающегося исключений. На практике она позволяет указывать действия по очистке, которые должны происходить всегда, такие как закрытие файлов и разрыв соединений с сервером, когда они требуются.
Обратите внимание, что в Python 2.4 и предшествующих версиях конструкция finally не может применяться в операторе try, где есть except и else, а потому в случае использования более старого выпуска вариант try/finally лучше считать отдельной формой оператора. Тем не менее, в Python 2.5 и последующих версиях finally может появляться вместе с except и else, поэтому в наши дни действительно существует единственный оператор try с множеством необязательных конструкций (что мы вскоре обсудим). Однако какую бы версию вы не применяли, конструкция finally по-прежнему служит той же самой цели — указание действия по “очистке”, которые должны выполняться всегда независимо от любых исключений.
Как будет показано далее в главе, начиная с версий Python 2.6 и Python 8.0, новый оператор with и его диспетчеры контекстов предлагают основанный на объектах способ выполнения похожей работы для действий при выходе. В отличие от finally этот новый оператор также поддерживает действия при входе, но его сфера ограничена объектами, которые реализуют используемый им протокол диспетчеров контекстов.
Пример: написание кода действий при завершении с помощью try/finally
В предыдущей главе приводилось несколько простых примеров try/finally. Ниже приведен более реалистичный пример, который иллюстрирует типичную роль данного оператора:
class MyError(Exception): pass
def stuff(file) : raise MyError()
file = open (' data’, 'w') # Открытие выходного файла
# (также может потерпеть неудачу)
try:
stuff (file) # Генерирует исключение
finally:
file.close() # Всегда закрывать файл, чтобы сбросить буферы
вывода
print('not reached') # Выполнение продолжается здесь, только если
# не было исключений
Когда функция stuff генерирует свое исключение, поток управления переходит к выполнению блока finally, чтобы закрыть файл. Затем исключение распространяется либо до еще одного try, либо до стандартного обработчика верхнего уровня, который выводит стандартное сообщение об ошибке и прекращает работу программы. Следовательно, оператор после этого try никогда не будет достигнут. Если функция stuff не сгенерирует исключение, то программа все же выполнит блок finally для закрытия файла, но затем продолжит работу ниже полного оператора try.
В этом особом случае мы поместили вызов функции обработки файла внутрь оператора try с конструкцией finally, чтобы обеспечить закрытие файла и потому финализацию независимо от того, генерирует функция исключение или нет. Таким образом, последующий код может быть уверен в том, что содержимое буфера вывода файла сбрасывается из памяти на диск. Аналогичная кодовая структура может гарантировать, что подключения к серверу закрываются и т.п.
Как известно из главы 9 первого тома, файловые объекты автоматически закрываются при сборке мусора в стандартном Python (CPython), что очень полезно для временных файловых объектов, которые не присваивались переменным. Тем не менее, не всегда легко спрогнозировать, когда произойдет сборка мусора, особенно в крупных программах или альтернативных реализациях Python с отличающимися политиками сборки мусора (скажем, Jython, РуРу). Оператор try делает закрытие файлов более явным и принадлежащим специфическому блоку кода. Он гарантирует, что файл будет закрыт при выходе из блока безотносительно к тому, произошло исключение или нет.
Функция из рассмотренного примера не так уж и полезна (она всего лишь генерирует исключение), но помещение вызовов внутрь операторов try/finally является хорошим способом обеспечения того, что ваши действия по закрытию при завершении всегда выполняются. И снова интерпретатор Python всегда запускает код в блоках finally независимо от того, возникало исключение в блоке try или нет \
Обратите внимание на то, что определяемое пользователем исключение здесь снова реализовано с помощью класса — как будет более формально описано в главе 35, в Python 2.6, 3.0 и последующих выпусках в обеих линейках все исключения обязаны быть экземплярами классов.
Унифицированный оператор try/except/finally
Во всех версиях, предшествующих Python 2.5 (приблизительно для 15 лет его существования), оператор try поступал в двух разновидностях, которые на самом деле были двумя отдельными операторами. Мы могли применять либо finally, гарантируя, что код очистки всегда выполняется, либо записывать блоки except для перехвата и восстановления после специфических исключений и дополнительно указывать конструкцию else, подлежащую выполнению, если исключения не возникали.
Другими словами, конструкция finally не могла смешиваться с except и else. Отчасти причиной были проблемы реализации, а частично то, что смысл их смешивания казался неясным — перехват и восстановление после исключений выглядело несовместимой концепцией с выполнением действий очистки.
Тем не менее, в Python 2.5 и последующих версиях два оператора объединены. В наши дни мы можем смешивать конструкции finally, except и else в том же самом операторе — отчасти из-за похожей полезности в языке Java. То есть теперь оператор можно записывать в такой форме:
try: # Объединенная форма
главное-действие
except Exceptionl: обработчик1
except Exception2: # Перехват исключений
обработчик2
# Обработчик для случая отсутствия исключений # finally охватывает все остальное
else:
блок-else
finally:
блок-finally
Первым выполняется код в блоке главное-действие оператора, как обычно. Если этот код генерирует исключение, тогда друг за другом проверяются все блоки except в поисках совпадения со сгенерированным исключением. Если было сгенерировано исключение Exceptionl, то выполняется блок обработчик!', если Exception2, то блок обработчик2 и т.д. Если никакие исключения не генерировались, тогда выполняется блок-else.
Независимо от того, что произошло ранее, блок-finally выполняется один раз по завершении блока главное-действие и после обработки любых сгенерированных исключений. Фактически код в блок-finally будет выполняться, даже когда в обработчике исключений или в блок-else возникла ошибка и сгенерировано новое исключение.
Как всегда, конструкция finally не заканчивает исключение — если исключение активно во время выполнения блок-finally, оно продолжает распространяться после того, как блок-finally выполнен, а поток управления переходит куда-то в другое место внутри программы (в еще один try или в стандартный обработчик верхнего уровня). Если при выполнении блока finally активных исключений нет, то поток управления возобновляет работу после полного оператора try.
Совокупный эффект в том, что блок finally выполняется всегда, не считаясь со следующими условиями:
• в главном действии произошло исключение, которое было обработано;
• в главном действии произошло исключение, которое не было обработано;
• в главном действии исключения не возникали;
• в одном из обработчиков было сгенерировано новое исключение.
Опять-таки конструкция finally предназначена для указания действий очистки, которые всегда должны совершаться при выходе из try, независимо от того, какие исключения были сгенерированы или обработаны.
Унифицированный синтаксис оператора try
При сочетании подобного рода оператор try обязан иметь либо except, либо finally, а порядок его частей должен выглядеть примерно так:
try -> except -> else -> finally
где конструкции else и finally необязательны, может быть ноль и более конструкций except, но при наличии else должна присутствовать хотя бы одна конструкция except. Вообще говоря, оператор try состоит из двух частей: конструкции except с необязательной конструкцией else и/или конструкции finally.
На самом деле синтаксическую форму объединенного оператора можно описать более точно следующим образом (квадратные скобки обозначают необязательные части, а символ звездочки — наличие нуля и более конструкций):
try: # Формат 1
операторы
except [тип [as значение]]: # [тип [, значение] ] в Python 2.Х
опера торы [except [тип [as значение]]: операторы] *
[else:
операторы]
[finally:
опера торы]
try: # Формат 2
опера торы finally:
операторы
В соответствии с этими правилами конструкция else может появляться только при наличии, по меньшей мере, одной конструкции except, вдобавок всегда допускается смешивать except и finally независимо от того, имеется else или нет. Можно также смешивать finally и else, но только при наличии except (хотя except разрешено опускать имя исключения, чтобы перехватывать все и запускать описанный позже оператор raise для повторной генерации текущего исключения). Если вы нарушите любое из приведенных правил упорядочения, тогда до запуска вашего кода Python сгенерирует исключение, связанное с синтаксической ошибкой.
Комбинирование finally и except за счет вложения
До выхода Python 2.5 фактически было возможно комбинировать конструкции finally и except в try, синтаксически вкладывая try/except внутрь блока try оператора try/finally. Мы исследуем эту методику более полно в главе 36, но ее основы помогут прояснить смысл комбинированного оператора try — следующий код дает тот же самый результат, что и новая объединенная форма, показанная в начале раздела:
try: # Вложенный эквивалент объединенной формы
try:
главное-действие except Exceptionl: обработчик1 except Exception2: обработчик2
else:
ошибки -о тсутствуют finally: очистка
И снова блок finally всегда запускается при выходе независимо от того, что происходило в главном действии и какие обработчики исключений выполнялись во вложенном операторе try (отследите приведенные ранее четыре сценария и убедитесь, что здесь все работает одинаково). Поскольку else всегда требует except, такая вложенная форма даже поддерживает те же самые ограничения смешивания, присущие унифицированной форме оператора, которые были кратко описаны в предыдущем разделе.
Однако вложенный эквивалент некоторым представляется менее ясным и требует большего объема кода, чем новая объединенная форма — несмотря на всего лишь одну добавочную строку из четырех символов плюс отступы. Смешивание finally в том же самом операторе значительно облегчает написание и восприятие кода и в наши дни является предпочитаемой методикой.
Пример унифицированного оператора try
Ниже предлагается демонстрация работы объединенной формы оператора try. В файле mergedexc.py реализованы четыре распространенных сценария с операторами print, которые описывают содержание каждого:
# Файл mergedexc.ру (Python З.Х + 2.Х) sep = ' - ' * 45 + '\п'
# Исключение генерируется и перехватывается print(sep + 'EXCEPTION RAISED AND CAUGHT’) try:
x = 'spam'[99] except IndexError:
print('except run') # выполняется except
finally:
print('finally run') # выполняется finally
print('after run') # после выполнения
# Исключения не генерируются print(sep + 'NO EXCEPTION RAISED') try:
x = 'spam'[3] except IndexError:
print('except run') # выполняется except
finally:
print('finally run') # выполняется finally
print('after run') # после выполнения
# Исключения не генерируются, с конструкцией else print(sep + 'NO EXCEPTION RAISED, WITH ELSE')
try:
x = 'spam'[3] except IndexError:
print('except run') else:
print('else run') finally:
print('finally run') print('after run')
# выполняется except
# выполняется else
# выполняется finally
# после выполнения
# Исключение генерируется, но не перехватывается print(sep + 'EXCEPTION RAISED BUT NOT CAUGHT') try:
x = 1 / 0 except IndexError:
print('except run') # выполняется except
finally:
print('finally run') # выполняется finally
print('after run') # после выполнения
Запуск этого кода в версии Python 3.7 дает показанный далее вывод; в Python 2.Х поведение кода и вывод будут такими же, потому что каждый вызов print выводит одиночный элемент, но сообщение об ошибке слегка отличается. Отследите код, чтобы понять, каким образом обработка исключений производит вывод каждого из четырех тестов:
c:\code> ру -3 mergedexc.ру
EXCEPTION RAISED AND except run finally run after runCAUGHT
NO EXCEPTION finally run after runRAISED
NO EXCEPTION else run finally run after runRAISED,WITH ELSE
EXCEPTION RAISED BUT NOT CAUGHT
finally run
Traceback (most recent call last):
File "C:\Code\mergedexc.py”, line 39, in <module> x = 1 / 0
ZeroDivisionError: division by zero
Трассировка (самый последний вызов указан последним) :
Файл "С: \Code\mergedexc.ру", строка 39, в <модуль> х = 1 / О
Ошибка деления на ноль: деление на ноль
В главном действии примера используются встроенные операции для генерирования (или нет) исключений, и он полагается на тот факт, что в ходе выполнения кода интерпретатор Python всегда делает проверки на предмет ошибок. В следующем разделе объясняется, как генерировать исключения вручную.
Оператор raise
Чтобы генерировать исключения явно, можно записывать операторы raise. Их общая форма проста — оператор raise состоит из слова raise, за которым дополнительно указывается класс или экземпляр класса генерируемого исключения:
raise экземпляр # Генерирует экземпляр класса
raise класс # Создает и генерирует экземпляр класса: создает экземпляр
raise # Повторно генерирует самое последнее исключение
Как упоминалось ранее, в Python 2.6, 3.0 и последующих версиях исключения всегда являются экземплярами классов. Таким образом, первая форма raise считается наиболее распространенной — мы напрямую предоставляем экземпляр, созданный либо перед raise, либо внутри самого оператора raise. Если взамен мы передаем класс, тогда интерпретатор Python вызывает конструктор класса без аргументов для создания экземпляра исключения, подлежащего генерации; такая форма эквивалентна добавлению круглых скобок к ссылке на класс. Последняя форма повторно генерирует самое последнее исключение; она обычно применяется в обработчиках исключений для распространения исключений, которые были перехвачены.
fjZ ! Примечание, касающееся нестыковки версий. В Python З.Х больше не поддерживается форма raise Exc, Args, которая по-прежнему доступна в
j Python 2.Х. Вместо нее используйте в Python З.Х форму с вызовом для
ш создания экземпляра raise Exc {Args), описанную в настоящей книге. Эквивалентная форма с запятой в Python 2.Х является унаследованным синтаксисом, который предоставляется для совместимости с теперь исчезнувшей моделью исключений на основе строк и объявлен устаревшим в Python 2.Х. В случае применения форма с запятой преобразуется в форму с вызовом Python З.Х.
Как и в более ранних выпусках, форма raise Exc также позволяет указывать имя класса — в обеих линейках она преобразуется в raise Ехс{), вызывая конструктор класса без аргументов. Помимо своего исчезнувшего синтаксиса с запятой оператор raise в Python 2.Х также допускает исключения на основе строк или классов, но первый из двух вариантов удален в Python 2.6, объявлен устаревшим в Python 2.5 и подробно здесь не рассматривается, а лишь кратко упоминается в следующей главе. В наши дни для новых исключений используйте классы.
Генерация исключений
Чтобы сделать все более понятным, давайте обратимся к нескольким примерам. Со встроенными исключениями показанные далее две формы эквивалентны — обе генерируют экземпляр указанного класса исключения, но первая создает экземпляр неявно:
raise IndexError # Класс (экземпляр создается неявно)
raise IndexError () # Экземпляр (создается явно в операторе)
Мы также можем создать экземпляр заблаговременно — поскольку оператор raise принимает любой вид ссылки на объект, следующие два примера генерируют IndexError в точности как предыдущие два:
exc = IndexError () # Создать экземпляр заблаговременно
raise exc
exes = [IndexError, TypeError] raise exes[0]
Когда генерируется исключение, интерпретатор Python отправляет вместе с ним сгенерированный экземпляр. Если оператор try содержит конструкцию except имя as Х:у тогда переменной X будет присвоен экземпляр, доставленный
raise:
try:
except IndexError as X: # X присваивается объект сгенерированного экземпляра
Ключевое слово as в обработчике try необязательно (если оно опущено, то экземпляр просто не присваивается имени), но его добавление дает обработчику возможность доступа к данным экземпляра и методам в классе исключения.
Эта модель работает и для определяемых пользователем исключений, реализованных посредством классов — скажем, вот пример передачи аргумента конструктору класса исключения, который становится доступным в обработчике через присвоенный экземпляр:
class MyExc(Exception): pass
raise MyExc (1 spam') # Класс исключения с аргументом конструктора
try:
except MyExc as X: # Атрибуты экземпляра, доступные в обработчике
print(X.args)
Тем не менее, мы не будем посягать здесь на тему следующей главы, где и будут представлены дальнейшие детали.
Независимо от того, как вы указываете исключения, они всегда идентифицируются объектами экземпляров классов, и любой заданный момент времени может быть активным самое большее один такой объект. Будучи однажды перехваченным конструкцией except где-нибудь в программе, исключение исчезает (т.е. не будет передаваться еще одному оператору try), если только оно не генерируется повторно другим оператором raise или возникшей ошибкой.
Области видимости и переменные except в try
Мы исследуем объекты исключений более глубоко в следующей главе. Однако теперь, увидев переменную as в действии, мы можем окончательно прояснить специфичную к версиям проблему с областями видимости, которая была подытожена в главе 17 первого тома. В Python 2.Х имя переменной со ссылкой на исключение в конструкции except локализуется внутри самой конструкции и доступно после выполнения ассоциированного блока:
с:\code> ру -2 >>> try:
... 1/0
. . . except Exception as X: # Python 2.X не локализует X в любом случае ... print X
integer division or modulo by zero
целочисленное деление или деление по модулю на ноль »> X
ZeroDivisionError('integer division or modulo by zero',)
Это верно в Python 2.X независимо от того, применяется стиль as из Python З.Х или более ранний синтаксис с запятой:
>>> try:
... 1/0
. . . except Exception, X:
. . . print X
integer division or modulo by zero
целочисленное деление или деление по модулю на ноль »> X
ZeroDivisionError('integer division or modulo by zero',)
И напротив, Python З.Х локализует имя переменной со ссылкой на исключение внутри блока except — после выхода из блока переменная не будет доступной почти как временная переменная цикла в выражениях включений Python З.Х (как отмечалось ранее, Python З.Х также не воспринимает в except синтаксис с запятыми из Python 2.Х):
с: \code> ру -3 >>> try:
... 1/0
. . . except Exception, X:
SyntaxError: invalid syntax
Синтаксическая ошибка: недопустимый синтаксис
»> try:
... 1/0
. . . except Exception as X: # Python З.Х локализует имена as внутри блока except . . . print(X)
division by zero деление на ноль »> X
NameError: name 'X' is not defined Ошибка в имени: имя X не определено
Тем не менее, в отличие от переменных цикла в выражениях включений в Python
З.Х после выхода из блока except эта переменная удаляется. Так происходит оттого, что в противном случае она сохранила бы ссылку на стек вызовов времени выполнения, которая отложила бы сборку мусора, оставив выделенным избыточное пространство памяти. Однако удаление переменной происходит, даже если вы используете имя где-то в другом месте, и является более крайней политикой, чем применяемая для включений:
»> X = 99 >» try:
... 1/0
. . . except Exception as X: # Python З.Х локализует переменную X
# удаляет ее при выходе!
... print(X)
division by zero деление на ноль »> X
NameError: name ’X' is not defined Ошибка в имени: имя X не определено »> X = 99
>>> {X for X in 'spam'} # Python 2.X/3.X только локализует X, но не удаляет { 's', ' а ', ' р ’, 'т' }
»> X 99
Из-за этого вы обычно должны использовать в конструкциях except оператора try уникальные имена переменных, хотя они и локализуются внутри областей видимости. Если вам необходимо ссылаться на экземпляр исключения после оператора try, тогда просто присвойте его еще одному имени, которое не будет автоматически удаляться:
>>> try:
... 1/0
. . . except Exception as X: # Python удаляет эту ссылку . . . print (X)
. . . Saveit = X # Присвоить экземпляр исключения для его
# сохранения при необходимости
division by zero деление на ноль >» X
NameError: name 'X' is not defined Ошибка в имени: имя X не определено »> Saveit
ZeroDivisionError('division by zero',)
Распространение исключений с помощью raise
Оператор raise несколько более функционален, чем мы видели до сих пор. Например, оператор raise, который не содержит имени исключения или добавочного значения данных, просто повторно генерирует текущее исключение. Такая форма обычно применяется, когда исключение необходимо перехватить и обработать, но его исчезновение в коде нежелательно:
»> try:
. . . raise IndexError('spam’) # Исключения запоминают аргументы . . . except IndexError:
... print(’propagating') # распространение
... raise # Повторная генерация самого последнего исключения
propagating
Traceback (most recent call last) :
File "<stdin>", line 2, in <module>
IndexError: spam распространение
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 2, в <модуль>
Ошибка индекса: spam
Выполнение raise подобным способом повторно генерирует исключение и передает его более высокому обработчику (или стандартному обработчику верхнего уровня, который выводит стандартное сообщение об ошибке и останавливает программу). Обратите внимание на отображение в сообщении об ошибке аргумента, переданного классу исключения; вы узнаете, почему так происходит, в следующей главе.
Сцепление исключений в Python З.Х: raise from
Исключения иногда могут генерироваться в ответ на другие исключения — как преднамеренно, так и по причине новых ошибок в программе. Для поддержки полноценного обнаружения в таких случаях в Python З.Х (но не в Python 2.Х) операторам raise также разрешено иметь дополнительную конструкцию from:
raise новое-исключение from другое-исключение
Когда конструкция from используется в явном запросе raise, следующее за from выражение указывает еще один класс или экземпляр для присоединения к атрибуту
_cause_нового генерируемого исключения. Если сгенерированное исключение не
перехвачено, тогда интерпретатор Python выводит оба исключения как часть стандартного сообщения об ошибке:
>» try:
... 1/0
. . . except Exception as E:
. .. raise TypeError('Bad') from E # Явно сцепленные исключения
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last) :
File "<stdin>", line 4, in <module>
TypeError: Bad
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 2, в <модуль>
Ошибка деления на ноль: деление на ноль
Вышеприведенное исключение было непосредственной причиной следующего исключения:
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 4, в <модуль>
Ошибка типа: Bad
Когда из-за ошибки в программе внутри обработчика исключений неявно генерируется исключение, автоматически соблюдается похожая процедура: предыдущее исключение присоединяется к атрибуту_context_нового исключения и снова отображается в стандартном сообщении об ошибке, если оно не было перехвачено:
»> try:
... 1/0 . . . except:
... badname # Неявно сцепленные исключения
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last) :
File "<stdin>", line 4, in <module>
NameError: name 'badname' is not defined Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 2, в <модуль>
Ошибка деления на ноль: деление на ноль
Во время обработки вышеприведенного исключения возникло еще одно исключение:
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 2, в <модуль>
Ошибка в имени: имя badname не определено
В обоих случаях из-за того, что объекты первоначальных исключений, присоединенные к объектам новых исключений, сами могут иметь присоединенные причины, цепочка причинно-следственной зависимости способна быть произвольно длинной и полностью отображаться в сообщениях об ошибках. То есть сообщения об ошибках могут содержать сведения о более чем двух исключениях. Совокупный эффект в явном и неявном контексте состоит в том, что программисты знают все вовлеченные исключения, когда одно исключение инициирует другое:
»> try:
. . . try:
. . . raise IndexError ()
. . . except Exception as E:
. . . raise TypeError() from E
. . . except Exception as E:
... raise SyntaxError() from E
Traceback (most recent call last) :
File "<stdin>", line 3, in <module>
IndexError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
TypeError
The above exception was the direct cause of the following exception:
Traceback (most recent call last) :
File "<stdin>’,/ line 7, in <module>
SyntaxError: None
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 3, в <модуль>
Ошибка индекса
Вышеприведенное исключение было непосредственной причиной следующего исключения:
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка Ъ, в <модуль>
Ошибка типа
Вышеприведенное исключение было непосредственной причиной следующего исключения:
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 1, в <модуль>
Синтаксическая ошибка: None
Код вроде показанного ниже схожим образом отображает три исключения, несмотря на неявную генерацию:
try:
try:
1/0 except:
badname
except:
open('nonesuch')
Как и унифицированный оператор try, сцепленные исключения полезны подобно их аналогам в других языках (в том числе Java и С#), хотя неясно, из каких языков они были позаимствованы. В Python исключения все еще являются не вполне понятным расширением, поэтому за дополнительными сведениями обращайтесь в руководства по языку. На самом деле согласно следующей врезке “На заметку!” в версии Python 3.3 появился способ подавления вывода сцепленных исключений.
Подавление вывода сцепленных исключений в Python 3: raise from None.
В Python 3.3 была представлена новая форма синтаксиса — указание None для имени исключения в операторе raise from: raise новое-исключение from None
Она позволяет запретить отображение контекста сцепленных исключений, описанного в предыдущем разделе. В итоге сообщения об ошибках становятся менее загроможденными в приложениях, которые выполняют преобразования между типами исключений наряду с обработкой цепочек исключений.
Оператор assert
В качестве довольно особого случая для целей отладки в Python предусмотрен оператор assert. По большей части assert представляет собой синтаксическое сокращение для распространенной схемы применения raise и может считаться условным оператором raise. Оператор вида:
assert test, data # Часть data является необязательной работает аналогично следующему коду:
if _debug_:
if not test:
raise AssertionError(data)
Другими словами, если в результате вычисления test получается False, тогда интерпретатор Python генерирует исключение: элемент data (если предоставлен) используется как аргумент для конструктора класса исключения. Подобно всем исключениям AssertionError прекратит выполнение программы, если не перехватить его с помощью try, и элемент data будет отображаться в виде части стандартного сообщения об ошибке.
Кроме того, операторы assert могут быть удалены из байткода скомпилированной программы в случае применения флага командной строки -О компилятора Python, что оптимизирует программу. AssertionError является встроенным исключением,
а флаг_debug_— встроенным именем, которое автоматически устанавливается
в True, если только не используется флаг -О. Применяйте командную строку вроде python -О main.py для запуска программы в оптимизированном режиме и отключения (а потому пропуска) утверждений.
Пример: улавливание нарушений ограничений (но не ошибок!)
Утверждения обычно используются для того, чтобы контролировать соблюдение условий в программах на стадии разработки. В отображаемые ими сообщения об ошибках автоматически включается информация о строке исходного кода и значение, указанное в операторе assert. Возьмем файл asserter .ру:
def f(х):
assert х < 0, 'х must be negative’ # х должно быть отрицательным return х ** 2
% python
>>> import asserter
>» asserter .f (1)
Traceback (most recent call last) :
File "<stdin>", line 1, in <module> asserter.f(1)
File "C:\Code\asserter.pyM, line 2, in f assert x < 0, 'x must be negative'
AssertionError: x must be negative
Трассировка (самый последний вызов указан последним) :
Файл ”<stdin>”, строка 1, в <модуль> asserter. f (1)
Файл "C:\Code\asserter.ру", строка 2, в f assert х < 0, 'х must be negative’
Ошибка утверждения: х должно быть отрицательным
Важно помнить о том, что оператор assert предназначен главным образом для улавливания нарушений ограничений, определяемых пользователем, а не для перехвата подлинных программных ошибок. Поскольку интерпретатор Python самостоятельно отлавливает программные ошибки, обычно нет никакой необходимости записывать операторы assert для перехвата таких вещей, как индексы, выходящие за допустимые границы, несовпадения типов и деление на ноль:
def reciprocal(х):
assert х != 0 # В целом бесполезное утверждение!
return 1 / х # Python автоматически проверяет на предмет равенства нулю
Такие сценарии применения assert, как правило, излишни — из-за того, что интерпретатор Python генерирует исключения при возникновении ошибок автоматически, вы с тем же успехом могли бы позволить ему делать эту работу за вас. Как правило, вам не нужно делать явную проверку на предмет ошибок в собственном коде.
Разумеется, у большинства правил существуют отклонения — как предполагалось ранее в книге, если функция должна выполнить длительные или невосстанавливае-мые действия, прежде чем она доберется до места, где будет сгенерировано исключение, то вам по-прежнему может требоваться проверка на предмет ошибок. Тем не менее, даже в этом случае будьте осторожны, чтобы не сделать проверки чрезмерно специфическими или ограничивающими, иначе вы снизите полезность своего кода.
Еще один распространенный сценарий использования assert демонстрировался в примере абстрактного суперкласса в главе 29; там оператор assert применялся для того, чтобы вызовы неопределенных методов терпели неудачу с выводом сообщения. Это редкий, но полезный инструмент.
Диспетчеры контекстов with/as
В версиях Python 2.6 и Python 3.0 был введен новый оператор, связанный с исключениями — with вместе с его необязательной конструкцией as. Оператор with/as предназначен для работы с объектами диспетчеров контекстов, которые поддерживают новый основанный на методах протокол, по духу похожий на способ работы с методами итерационных инструментов в протоколе итерации. Данная возможность доступна как вариант в версии Python 2.5, но должна быть включена посредством оператора import вида:
from_future_ import with_statement
Оператор with также подобен оператору using в языке С#. Хотя тема диспетчеров контекстов, вообще говоря, необязательна и ориентирована на сложные инструменты (а потому является кандидатом на рассмотрение в следующей части книги), диспетчеры контекстов достаточно легковесны и полезны, чтобы объединить их здесь с остатком инструментального комплекта для работы с исключениями.
Выражаясь кратко, оператор with/as задуман как альтернатива распространенной идиоме использования try/finally; подобно try/finally оператор with в значительной степени предназначен для указания действий стадии завершения или “очистки”, которые должны выполняться независимо от того, возникало ли исключение в течение шага обработки.
В отличие от try/finally оператор with основан на объектном протоколе для указания действий, подлежащих выполнению вокруг блока кода. Это делает оператор with менее универсальным, квалифицирует его как избыточный в ролях с завершением и требует реализации классов для объектов, которые не поддерживают его протокол. С другой стороны, with также поддерживает действия при входе, способен сократить размер кода и позволяет управлять контекстами кода с помощью полноценного ООП.
Python расширяет диспетчерами контекстов ряд встроенных инструментов, таких как файлы, которые автоматически закрываются, и потоки, которые автоматически блокируются и деблокируются, но программисты могут реализовывать собственные диспетчеры контекстов с применением классов. Давайте кратко рассмотрим оператор и его неявный протокол.
Базовое использование
Ниже показан базовый формат оператора with с необязательной частью в квадратных скобках:
with выражение [as переменная] : блок-with
Здесь предполагается, что выражение возвращает объект, поддерживающий протокол управления контекстами (который вскоре будет обсуждаться). Этот объект может также возвращать значение, которое будет присвоено имени переменная при наличии необязательной конструкции as.
Обратите внимание, что переменной не обязательно присваивается результат выражения; результатом выражения является объект, который поддерживает протокол управления контекстами, и переменной может быть присвоено что-то другое, предназначенное для применения внутри оператора. Затем объект, возвращенный выражением, может выполнить код запуска перед началом блока-with, а также код завершения после окончания блока-with независимо от того, генерировалось ли в блоке исключение.
Некоторые встроенные объекты Python были дополнены поддержкой протокола управления контекстами, а потому могут использоваться с оператором with. Скажем, файловые объекты (описанные в главе 9 первого тома) имеют диспетчер контекста, который автоматически закрывает файл после блока with безотносительно к тому, генерировалось ли исключение, и независимо от того, если или когда версия Python, выполняющая код, может закрыть его автоматически:
with open(г'С:\misc\data1) as myfile: for line in myfile: print(line)
. . .дополнительный код. . .
Вызов open возвращает простой файловый объект, который присваивается имени myfile. Мы можем применять myfile с обычными файловыми инструментами — в данном случае файловый итератор читает строка за строкой в цикле for.
Однако этот объект поддерживает протокол управления контекстами, используемый оператором with. После выполнения такого оператора with механизм управления контекстами гарантирует, что файловый объект, на который ссылается myfile, автоматически закрывается, даже если во время обработки файла в цикле for возникло исключение.
Хотя файловые объекты могут автоматически закрываться при сборке мусора, не всегда легко узнать, когда она случится, особенно когда применяются альтернативные реализации Python. Оператор with в такой роли представляет собой альтернативу, которая обеспечивает нам уверенность в том, что закрытие произойдет после выполнения специфического блока кода.
Как было показано ранее, мы можем достичь похожего эффекта с помощью более универсального и явного оператора try/finally, но в рассматриваемой ситуации он требует три дополнительных строки административного кода (четыре вместо только одной):
myfile = open(г1 С:\misc\data’ )
try:
for line in myfile: print(line)
...дополнительный код...
finally:
myfile.close()
Мы не раскрываем в книге модули многопоточной обработки Python (ищите сведения о них в книгах прикладного уровня наподобие Programming Python (http: // 80596158101)). Тем не менее, определяемые ими объекты блокировок и условной синхронизации также могут использоваться с оператором with, потому что они поддерживают протокол управления контекстами — в этом случае добавление действий входа и выхода вокруг блока:
lock = threading.Lock() # После import threading
with lock:
# критический раздел кода . . . доступ к разделяемым ресурсам. . .
Здесь механизм управления контекстами гарантирует, что блокировка будет автоматически получена перед выполнением блока и освобождена после окончания блока независимо от исходов исключений.
Как было представлено в главе 5 первого тома, модуль decimal также применяет диспетчеры контекстов для упрощения сохранения и восстановления текущего контекста десятичных чисел, который указывает точность и характеристики округления для вычислений:
with decimal.localcontext() as ctx: # После import decimal ctx.prec = 2
x = decimal.Decimal('1.00 ' ) / decimal.Decimal('3.00')
После выполнения данного оператора состояние диспетчера текущего потока автоматически восстанавливается в то, которое было до начала оператора. Чтобы сделать то же самое посредством try/finally, нам пришлось бы сохранять контекст перед и восстанавливать его вручную после вложенного блока.
Протокол управления контекстами
Хотя некоторые встроенные типы снабжены диспетчерами контекстов, мы также можем создавать новые такие диспетчеры самостоятельно. Для реализации диспетчеров контекстов классы используют специальные методы, которые относятся к категории методов перегрузки операций и позволяют взаимодействовать с оператором with. Интерфейс, ожидаемый от применяемых в операторах with объектов, довольно сложен, и большинству программистов необходимо лишь знать, как использовать существующие диспетчеры контекстов. Однако у разработчиков инструментов может возникнуть потребность в написании новых диспетчеров контекстов, специфичных для приложений, поэтому давайте бегло взглянем, что им предстоит делать.
Вот как в действительности работает оператор with.
1. Выражение вычисляется, давая в результате объект диспетчера контекста, который обязан иметь методы_enter_и_exit_.
2. Вызывается метод_enter_диспетчера контекста. Возвращаемое им значение
присваивается переменной в конструкции as при ее наличии либо попросту отбрасывается.
3. Выполняется код во вложенном блоке with.
4. Если в блоке with возникает исключение, тогда вызывается метод
_exit_(type, value, traceback) с передачей ему деталей исключения. Это
те же самые значения, которые возвращает функция sys . exc_info, описанная в руководствах по Python и позже в данной части книги. Если метод_exit_
возвращает значение False, тогда исключение генерируется повторно; иначе оно заканчивается. Обычно исключение должно быть сгенерировано заново, чтобы оно распространилось за пределы оператора with.
5. Если в блоке with исключение не возникало, то метод_exit_все равно вызывается, но для всех его аргументов type, value и traceback передаются None.
Рассмотрим небольшую иллюстрацию протокола в действии. В файле withas .ру с показанным далее содержимым определяется объект диспетчера контекста, который отслеживает вход и выход из блока with в любом операторе with, где он применяется:
class TraceBlock:
def message(self, arg) : print(1 running 1 + arg)
# выполнение
def _enter_(self) :
print('starting with block') return self
# начало блока
def _exit_(self, exc_type, exc_value, exc_tb):
if exc_type is None: print(1 exited normally\n') else:
# нормальный выход
+ str(exc_type))
print('raise an exception! return False
# генерация исключения
# Распространение
if _name_ == '_main_' :
with TraceBlock () as action: action.message('test 1') print('reached1) with TraceBlock () as action: action.message(1 test 2') raise TypeError print('not reached1)
# достигнуто
# не достигнуто
Обратите внимание, что метод_exit_класса TraceBlock возвращает False,
чтобы исключение распространялось; удаление оператора return дало бы тот же самый эффект, т.к. стандартное возвращаемое значение None функций по определению
равно False. Кроме того, метод_enter_возвращает self в качестве объекта для
присваивания переменной as; в других сценариях использования может возвращаться совершенно другой объект.
Во время выполнения диспетчер контекста отслеживает вход и выход из блока операторов with посредством своих методов_enter_и_exit_. Ниже приведена демонстрация сценария в работе под управлением Python З.Х или 2.Х (как обычно, в Python 2.Х вывод слегка отличается, и сценарий допускает запуск в Python 2.6, 2.7 и
2.5 при включенной возможности):
c:\code> ру -3 withas.ру
starting with block running test 1 reached
exited normally
starting with block running test 2
raise an exception! <class 'TypeError'>
Traceback (most recent call last) :
File "withas.py", line 22, in <module> raise TypeError TypeError
Трассировка (самый последний вызов указан последним) :
Файл "C:\Code\ withas .ру", строка 22, в <модуль> raise TypeError Ошибка типа
Диспетчеры контекстов также могут задействовать информацию о состоянии и наследование ООП, но являются относительно сложным механизмом, ориентированным на разработчиков инструментов, поэтому мы опускаем здесь остальные детали (за исчерпывающим описанием обращайтесь к стандартным руководствам по Python — например, существует библиотечный модуль context lib, который предлагает дополнительные инструменты для реализации диспетчеров контекстов). Для более простых целей оператор try/finally обеспечивает достаточную поддержку выполнения действий при завершении без необходимости в написании классов.
Множество диспетчеров контекстов в Python 3.1, 2.7 и последующих версиях
В версии Python 3.1 было введено расширение with, которое со временем появилось и в Python 2.7. В этих и последующих версиях Python в операторе with можно также указывать множество (иногда называемых “вложенными”) диспетчеров контекстов с помощью нового синтаксиса в виде запятой. Скажем, в показанном далее коде действия выхода обоих файловых объектов автоматически запускаются при выходе из блока операторов независимо от исходов исключений:
with open('data') as fin, open('res', 'w') as fout: for line in fin:
if 'some key' in line: fout.write(line)
Допускается указывать любой количество диспетчеров контекстов и множество элементов работают аналогично вложенным операторам with. В версиях Python, поддерживающих такую возможность, следующий код:
with А() as а, В () as Ь:
. . . операторы. . .
эквивалентен приведенному ниже коду, который также работает в Python 3.0 и 2.6:
with А() as а: with В() as b:
. . . операторы. . .
Дополнительные подробности можно найти в пояснительных записках к выпуску Python 3.1, но здесь мы кратко взглянем на данное расширение в действии. Чтобы реализовать параллельный просмотр двух файлов по строкам, в представленном далее коде применяется оператор with для открытия двух файлов и объединения их строк посредством zip без необходимости в ручном закрытии по завершении (предполагая, что оно обязательно):
>» with open (1 scriptl .ру') as fl, open ('script2 .py') as f2:
. . . for pair in zip(fl, f2) :
... print(pair)
('# A first Python script\n', 'import sys\n')
('import sys # Load a library module\n', 'print(sys.path)\n')
('print(sys.platform)\n', 'x = 2\n')
('print(2 ** 32) # Raise 2 to a power\n', 'print(x ** 32)\n')
Такую кодовую структуру можно использовать, например, для построчного сравнения двух текстовых файлов, заменив print оператором if и вызвав enumerate для номеров строк:
with open('scriptl .ру' ) as fl, open('script2 .py' ) as f2: for (linenum, (linel, line2)) in enumerate (zip (fl, f2)) : if linel != line2:
print('%s\n%r\n%r' % (linenum, linel, line2))
Тем не менее, предыдущий прием не так уж полезен в CPython, потому что входные файлы не требуют сбрасывания буферов и при освобождении занимаемой памяти файловые объекты автоматически закрываются, если они остались открытыми. В CPython код для параллельного просмотра можно упростить и занимаемая файловыми объектами память станет освобождаться немедленно:
for pair in zip (open (' scriptl .py' ) , open (' script2 . py' ) ) : # Тот же результат,
# автоматическое закрытие
print(pair)
С другой стороны, из-за отличий в сборщиках мусора альтернативные реализации наподобие РуРу и Jython могут требовать более прямого закрытия внутри циклов во избежание истощения системных ресурсов. Следующий код автоматически закрывает выходной файл, когда оператор заканчивается, чтобы любой буферизированный текст незамедлительно перемещался на диск:
>>> with open(1 script2.ру1) as fin, open('upper.py' , ’wf) as fout:
. . . for line in fin:
. . . fout.write(line.upper())
>>> print(open(1 upper.py').read())
IMPORT SYS PRINT(SYS.PATH)
X = 2
PRINT(X ** 32)
В обоих случаях мы можем взамен просто открывать файлы в отдельных операторах и при необходимости закрывать их после обработки, а в некоторых сценариях, вероятно, мы даже должны поступать так — нет никакого смысла применять операторы, перехватывающие исключение, если это означает, что программа все равно не работает!
fin = open('script2.ру') fout = open('upper .pyf, 'w')
for line in fin: # Тот же результат, что и в предыдущем коде,
# автоматическое закрытие
fout.write(line.upper())
Однако в случае, если программы должны продолжать работу после возникновения исключений, формы with также неявно перехватывают исключения и тем самым позволяют избежать написания оператора try/finally в ситуациях, когда закрытие обязательно. Эквивалентная реализация без with оказывается более явной, но требует заметно большего объема кода:
fin = open('script2.ру') fout = open('upper.ру', 'w')
try: # Тот же результат, но явное закрытие при возникновении ошибки
for line in fin:
fout.write(line.upper ())
finally:
fin.close() fout.close()
С другой стороны, try/finally является одиночным инструментом, применимым ко всем сценариям финализации, тогда как with добавляет второй инструмент, который может быть более компактным, но применяется только к определенным объектным типам и удваивает необходимую базу знаний для программистов. Как обычно, вам придется самостоятельно проанализировать все компромиссы.
Резюме
В главе мы рассматривали обработку исключений более подробно за счет исследования операторов Python, связанных с исключениями: try для перехвата, raise для генерации, assert для условной генерации и with для помещения блоков кода внутрь диспетчеров контекстов, которые задают действия входа и выхода.
До сих пор исключения, возможно, казались довольно легковесным инструментом и на самом деле таковыми они и являются; единственная сложность в том, как они идентифицируются. В следующей главе наше исследование продолжается описанием того, как реализовать сами объекты исключений; вы увидите, что классы позволяют создавать новые исключения, специфичные к разрабатываемым программам. Но сначала закрепите пройденный материал главы, ответив на контрольные вопросы.
Проверьте свои знания: контрольные вопросы
1. Для чего предназначен оператор try?
2. Назовите две распространенных вариации оператора try.
3. Для чего предназначен оператор raise?
4. Для чего предназначен оператор assert, и на какой другой оператор он похож?
5. Для чего предназначен оператор with/as, и на какой другой оператор он похож?
Проверьте свои знания: ответы
1. Оператор try осуществляет перехват и восстановление после исключений — он указывает блок кода, подлежащий запуску, и один или большее число обработчиков для исключений, которые могут возникнуть во время выполнения этого блока.
2. Двумя распространенными вариациями оператора try являются try/except/ else (для перехвата исключений) и try/finally (для указания действий очистки, которые должны происходить независимо от того, генерировалось исключение или нет). До Python 2.4 они были отдельными операторами, которые можно было комбинировать путем синтаксического вложения; в Python 2.5 и последующих версиях блоки except и finally могут смешиваться в одном операторе, поэтому обе формы оператора были объединены. В объединенной форме finally по-прежнему выполняется при выходе из try, не обращая внимания на то, какие исключения могли быть сгенерированы или обработаны. Фактически объ-
единенная форма эквивалентна вложению try/except/else в try/finally, а две вариации все еще исполняют логически отличающиеся роли.
3. Оператор raise генерирует (инициирует) исключение. Интерпретатор Python внутренне генерирует встроенные исключения при возникновении ошибок, но ваши сценарии с помощью raise тоже могут генерировать встроенные или определяемые пользователем исключения.
4. Оператор assert генерирует исключение AssertionError, если условие оказывается ложным. Он работает подобно условному оператору raise, помещенному внутрь оператора if, и может быть отключен с использованием флага -О.
5. Оператор with/as предназначен для автоматизации действий при запуске и завершении, которые должны происходить вокруг блока кода. Он примерно похож на оператор try/finally в том, что его действия выхода выполняются независимо от того, возникало исключение или нет, но делает возможным более развитый протокол на основе объектов для указания действий входа и выхода, а также способен сократить размер кода. Тем не менее, оператор with/as не настолько универсальный, т.к. он применим только к объектам, которые поддерживают его протокол; оператор try охватывает гораздо больше сценариев использования.
ГЛАВА 35
Назад: Основы исключений
Дальше: Объекты исключений