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

Объекты исключений

 

До сих пор я преднамеренно не давал определение того, чем фактически является исключение. Как подсказывалось в предыдущей главе, начиная с версий Python
2.6 и 3.0, встроенные и определяемые пользователем исключения идентифицируются объектами экземпляров классов. Именно они генерируются и распространяются в ходе обработки исключений и представляют собой источник класса, который сопоставляется с исключениями, указанными в операторах.
Хотя это означает, что для определения новых исключений в своих программах вы должны использовать ООП — и вводит зависимость от сведений, полное раскрытие которых было отложено до текущей части книги, — базирование исключений на классах и ООП сулит несколько перечисленных ниже преимуществ.
• Исключения на основе классов организованы по категориям. Исключения, реализованные в виде классов, поддерживают будущие изменения за счет предоставления категорий — добавление новых исключений впоследствии обычно не требует внесения изменений в операторы try.
• Исключения на основе классов обладают информацией состояния и поведением. Классы исключений предлагают естественное местоположение для хранения информации о контексте и инструменты для применения в обработчике try — экземпляры имеют доступ к присоединенной информации состояния и вызываемым методам.
• Исключения на основе классов поддерживают наследование. Основанные на классах исключения могут принимать участие в иерархиях наследования, чтобы получать и настраивать общее поведение — например, унаследованные методы отображения способны предоставлять общий внешний вид для сообщений об ошибках.
Благодаря описанным преимуществам исключения на основе классов хорошо поддерживают развитие программ и более крупные системы. Как обнаружится, по указанным выше причинам все встроенные исключения идентифицируются классами и организуются в дерево наследования. Вы можете делать то же самое с собственными исключениями, определяемыми пользователем.
Фактически в Python З.Х исследуемые здесь встроенные исключения оказываются неотъемлемой составляющей новых исключений, которые вы определяете. Поскольку Python З.Х требует, чтобы определяемые пользователем исключения наследовались от встроенных суперклассов исключений, предлагающих полезные стандартные методы для вывода и сохранения состояния, задача реализации исключений, определяемых пользователем, также предусматривает понимание ролей, исполняемых встроенными исключениями.
"it" : Примечание, касающееся нестыковки версий. В Python 2.6, 3.0 и последую-j щих версиях исключения должны определяться посредством классов. Вдобавок в Python З.Х классы исключений обязаны быть производными от ВСТроенного класса исключения BaseException, либо напрямую, либо косвенно. Как мы увидим, в большинстве программ реализуется наследование от подкласса Exception данного класса, чтобы поддерживать универсальные обработчики для нормальных типов исключений — его указание в обработчике обеспечит перехват всего, что должно перехватываться. Автономным классическим классам Python 2.Х также разрешено служить в качестве исключений, но, как и в Python З.Х, классы нового стиля обязаны быть унаследованными от встроенных классов исключений.
Исключения: назад в будущее
Когда-то давно (до версий Python 2.6 и 3.0) исключения можно было определять двумя разными способами, что усложняло операторы try, операторы raise и язык Python в целом. В наши дни существует только один способ их определения. И это хорошо: из языка исчез значительный объем хлама, накопленного ради обратной совместимости. Однако поскольку старый способ помогает объяснить, почему исключения стали такими, какими они есть сегодня, и потому, что невозможно полностью забыть историю чего-либо, используемого примерно миллионом людей в течение почти двух десятилетий, давайте начнем наше исследование настоящего с краткого экскурса в прошлое.
Строковые исключения канули в лету!
До выхода версий Python 2.6 и 3.0 исключения можно было определять с помощью экземпляров классов и строковых объектов. В версии Python 2.5 для исключений на основе строк начали выдаваться предупреждения об устаревании, а в версиях Python
2.6 и 3.0 они были удалены, поэтому в настоящее время вы должны применять исключения на основе классов, как показано в книге. Тем не менее, в случае работы с унаследованным кодом вы все еще можете сталкиваться со строковыми исключениями. Они также могут встречаться в книгах, руководствах и веб-ресурсах, созданных несколько лет тому назад (что в годах Python считается вечностью!).
Строковые исключения было легко использовать — подходила любая строка и они сопоставлялись по объектной идентичности, а не по значению (т.е. применялась операция is, не ==):
C:\code> C:\Python25\python
»> myexc = "My exception string" # Мы были настолько молодыми?. . .
»> try:
. . . raise myexc
. . . except myexc:
... print(1 caught1)
caught
Такую форму исключений удалили, потому что для более крупных программ и сопровождения кода она была не настолько хорошей, как классы. В современных версиях Python строковые исключения сами генерируют исключения:
С:\code> ру -3 >>> raise 'spam1
TypeError: exceptions must derive from BaseException
Ошибка типа: исключения должны быть унаследованными от BaseException
С: \code> ру -2 >>> raise 'spam'
TypeError: exceptions must be old-style classes or derived from BaseException, ...etc
Ошибка типа: исключения должны быть классами старого стиля или унаследованными от BaseException, . . .и т.д.
Несмотря на невозможность использования строковых исключений в наши дни, в действительности они обеспечивают естественную связку для представления модели исключений на основе классов.
Исключения на основе классов
Строки служили простым способом определения исключений. Однако, как было описано ранее, классы имеют ряд дополнительных преимуществ, которые заслуживают краткого обзора. Самое примечательное то, что они позволяют идентифицировать категории исключений, которые оказываются более гибкими в применении и сопровождении, чем простые строки. Кроме того, классы естественным образом позволяют присоединять информацию об исключениях и поддерживают наследование. Поскольку классы рассматриваются многими как лучший подход, теперь они являются обязательными.
Если оставить в стороне детали кода, то главное отличие между строковыми исключениями и исключениями на основе классов связано со способом генерации исключений и их сопоставления с конструкциями except в операторах try.
• Строковые исключения сравнивались по простой объектной идентичности: сгенерированное исключение сопоставлялось с конструкциями except посредством проверки is.
• Исключения на основе классов сопоставляются по отношениям с суперклассами: сгенерированное исключение соответствует конструкции except, если в ней присутствует класс экземпляра исключения или любой из его суперклассов.
То есть, когда в конструкции except оператора try указан суперкласс, она перехватывает экземпляры этого суперкласса, а также всех его подклассов ниже в дереве классов. Совокупный эффект состоит в том, что основанные на классах исключения естественным образом поддерживают построение иерархий исключений: суперклассы становятся названиями категорий, а подклассы — специфичными видами исключений внутри категории. За счет указания имени общего суперкласса конструкция except способна перехватывать целую категорию исключений — любой специфический подкласс будет давать соответствие.
В строковых исключениях концепция подобного рода отсутствовала: из-за того, что они сопоставлялись по простой объектной идентичности, не существовало прямого способа организации исключений в более гибкие категории или группы. В итоге обработчики исключений были связаны с наборами исключений таким образом, что затрудняли внесение изменений.
Кроме идеи с категориями исключения на основе классов эффективнее поддерживают информацию состояния исключений (присоединяемую к экземплярам) и позволяют исключениям участвовать в иерархиях наследования (чтобы получать общие линии поведения). Поскольку они обеспечивают все преимущества классов и ООП в целом, то предлагают более мощную альтернативу ныне несуществующей модели исключений на основе строк в обмен на скромный объем добавочного кода.
Реализация классов исключений
Давайте рассмотрим пример, чтобы выяснить, как исключения на основе классов воплощаются в коде. В файле classexc.py, содержимое которого приведено ниже, мы определяем суперкласс по имени General и два подкласса с именами Specificl и Specific2. В примере иллюстрируется понятие категорий исключений — General представляет собой название категории, а два его подкласса являются специфичными типами исключений внутри категории. Обработчики, которые перехватывают суперкласс General, будут перехватывать также любые его подклассы, в том числе Specificlи Specific2:
class General(Exception): pass class Specificl(General): pass class Specific2(General): pass
def raiserO () :
X = General () # Генерирует экземпляр суперкласса
raise X
def raiserl():
X = Specificl() # Генерирует экземпляр подкласса
raise X
def raiser2():
X = Specific2() # Генерирует экземпляр другого подкласса
raise X
for func in (raiserO, raiserl, raiser2) : try:
func()
except General: # Соответствует суперклассу General или любому его подклассу import sys
print(1 caught: %s' % sys.exc_info() [0])
C:\code> python classexc.py
caught: cclass 1_main_.General'>
caught: <class ’_main_.Specificl’>
caught: <class '_main_.Specific2'>
Код в основном прямолинеен, но есть несколько моментов, которые следует отметить.
Суперкласс Exception
Классы, используемые для построения деревьев категорий исключений, предъявляют очень мало требований — фактически в примере они по большей части пусты, с телами, в которых ничего не делается, а присутствует только pass. Тем не менее, обратите внимание на наследование класса верхнего уровня от встроенного класса Exception. В Python З.Х поступать так обязательно; в Python 2.Х автономным классическим классам также разрешено служить в качестве исключений, но классы нового стиля должны быть производными от встроенных классов исключений в точности как в Python З.Х. Хотя мы здесь это не задействуем, потому что суперкласс Exception предоставляет ряд полезных линий поведения, с которыми мы встретимся позже, неплохая идея наследовать от него в любой версии Python.
Генерация экземпляров
В коде мы обращаемся к классам с целью создания экземпляров для операторов raise. В рамках модели исключений, основанных на классах, мы всегда генерируем и перехватываем объект экземпляра класса. Если мы указываем в raise имя класса без круглых скобок, тогда интерпретатор Python вызывает конструктор класса, лишенный аргументов, чтобы создать для нас экземпляр. Экземпляры исключений могут создаваться перед raise, как делалось здесь, или внутри самого оператора raise.
Перехват категорий
Код содержит функции, которые генерируют экземпляры всех трех классов в виде исключений, а также оператор try верхнего уровня, вызывающий функции и перехватывающий исключения General. Тот же самый оператор try перехватывает и два специфичных исключения из-за того, что они являются подклассами General — членами его категории.
Детали исключений
В данном случае обработчик исключений применяет вызов sys.exc info — как будет более подробно обсуждаться в следующей главе, именно так мы можем захватывать самое недавнее исключение в обобщенной манере. Выражаясь кратко, первый элемент в его результате представляет собой класс сгенерированного исключения, а второй — действительный экземпляр. В универсальной конструкции except вроде показанной здесь, которая перехватывает все классы в категории, sys.exc info является одним способом выяснить, что в точности произошло. В рассмотренной конкретной ситуации вызов sys .exc info
эквивалентен извлечению атрибута_class_экземпляра. В следующей главе
мы увидим, что схема с sys . exc infо также часто используется с конструкциями except, которые перехватывают все исключения.
Последний пункт заслуживает дальнейшего объяснения. Когда исключение перехватывается, мы можем быть уверены в том, что сгенерированный экземпляр принадлежит классу, указанному в except, или одному из его более специфичных подклассов. По этой причине атрибут_class_экземпляра также дает тип исключения.
Скажем, показанный далее вариант из файла classexc2 .ру работает так же, как предыдущий пример — в нем применяется расширение as конструкции except для присваивания переменной фактически сгенерированного экземпляра:
class General(Exception): pass class Specificl(General): pass class Specific2(General): pass
def raiserO (): raise General () def raiserl(): raise Specificl () def raiser2(): raise Specific2()
for func in (raiserO, raiserl, raiser2) : try:
func() except General as X:
# X - сгенерированный экземпляр
# To же, что и sys.exc info() [0]
print('caught: %s' % X._class_)
Поскольку_class_можно использовать подобным образом для выяснения конкретного типа сгенерированного исключения, вызов sys. exc inf о более удобен в пустых конструкциях except, которые по-другому не могут получить доступ к экземпляру или его классу. Кроме того, более реалистичные программы обычно вообще не должны заботиться о том, какое конкретно исключение было сгенерировано — вызывая методы экземпляра класса исключения обобщенным образом, мы автоматически инициируем линию поведения, которая была настроена для сгенерированного исключения.
Более подробно об этом и sys.exc info пойдет речь в следующей главе; в главе 29 и в целом в части VI приведены сведения об атрибуте_class_в экземпляре,
а в предыдущей главе — обзор расширения as.
Для чего используются иерархии исключений?
Так как в примере из предыдущего раздела было только три возможных исключения, он не позволяет надлежащим образом оценить полезность исключений на основе классов. Фактически мы могли бы достичь того же результата, записав в конструкции except список имен исключений в круглых скобках:
try:
func()
except (General, Specificl, Specific2): # Перехватывать любое из них
Такой подход работает и для исчезнувшей модели строковых исключений. Однако для крупных или глубоких иерархий исключений может быть легче перехватывать категории, указывая классы, чем перечислять все члены категории в одиночной конструкции except. Пожалуй, более важно то, что по мере развития нужд программ вы можете расширять иерархии исключений, добавляя новые подклассы и не нарушая работу существующего кода.
Предположим для примера, что вы реализуете библиотеку численных расчетов на Python, ориентированную на применение многими программистами. Во время написания кода библиотеки вы идентифицируете две ситуации, когда что-то может пойти не так с числами — деление на моль и числовое переполнение. Вы документируете их как два автономных исключения, которые ваша библиотека может генерировать:
# mathlib.ру
class Divzero(Exception): pass class Oflow(Exception): pass
def func():
raise Divzero()
... и так далее. . .
Теперь при использовании вашей библиотеки программисты будут помещать вызовы функций или обращения к классам в операторы try, перехватывающие два упомянутых исключения; в конце концов, если они не перехватят ваши исключения, тогда исключения из библиотеки прекратят выполнение их кода:
# client.ру import mathlib try:
mathlib.func(...) except (mathlib.Divzero, mathlib.Oflow):
...выполнить обработку и восстановление...
Все работает нормально, и многие программисты начинают эксплуатировать вашу библиотеку. Тем не менее, спустя полгода вы пересматриваете ее (как склонны поступать все программисты!). Попутно вы идентифицируете новую ситуацию, когда что-то может пойти не так — скажем, потерю значимости — и добавляете ее как новое исключение:
# mathlib.ру
class Divzero(Exception): pass class Oflow(Exception): pass class Uflow(Exception): pass
К сожалению, после повторного выпуска кода вы создадите проблему с сопровождением для своих пользователей. Если они явно перечисляли ваши исключения, тогда им придется возвратиться к своему коду и внести изменения во всех местах, где производилось обращение к библиотеке, чтобы указать имя вновь добавленного исключения:
# client.ру try:
mathlib.func(...) except (mathlib.Divzero, mathlib.Oflow, mathlib.Uflow):
...выполнить обработку и восстановление...
Конечно, это не стоит считать концом света. Если ваша библиотека применяется только внутри организации, то вы можете внести изменения самостоятельно. Вы могли бы также поставлять сценарий на Python, который попытается автоматически скорректировать код подобного рода (вероятно, он будет состоять из нескольких десятков строк и окажется правильным решением, во всяком случае, иногда). Однако если многим программистам придется модифицировать все их операторы try каждый раз, когда вы изменяете свой набор исключений, то это будет не особенно изящная политика обновления.
Ваши пользователи могут попробовать избежать данной ловушки, записывая пустые конструкции except для перехвата всех возможных исключений:
# client.ру try:
mathlib.func(...)
except: # Перехватывать здесь все исключения (иди суперкласс Exception)
. . . выполнить обработку и восстановление. . .
Но этот обходной путь может перехватывать больше, чем предполагалось. В ситуациях вроде нехватки памяти, прерываний с клавиатуры (<Ctrl+C>), выходов в систему и даже опечаток в коде их собственного блока try будут возникать исключения, которые должны пропускаться, а не перехватываться и ошибочно трактоваться как ошибки из библиотеки. Указание суперкласса Exception улучшает положение, но он по-прежнему перехватывает и потому может маскировать ошибки в программе.
И действительно, в таком сценарии пользователи хотят перехватывать и восстанавливаться только после конкретных исключений, которые определены и документированы в плане генерирования. Если во время обращения к библиотеке возникает любое другое исключение, то можно ожидать, что оно отражает подлинный дефект в библиотеке (и вероятно повод связаться с поставщиком!). В качестве эмпирического правила запомните: в обработчиках исключений обычно лучше придерживаться конкретики, чем универсальности — идея, к которой мы еще вернемся при рассмотрении затруднений в следующей главе К
Что же тогда предпринять? Иерархии классов исключений полностью решают проблему. Вместо определения исключений вашей библиотеки как набора автономных классов организуйте их в виде дерева классов с общим суперклассом, охватывающим целую категорию:
# mathlib.ру
class NumErr(Exception): pass class Divzero(NumErr): pass class Oflow(NumErr): pass def func():
raise DivZero()
. . . и так далее. . .
В таком случае пользователям библиотеки просто нужно будет указывать общий суперкласс (т.е. категорию), чтобы перехватывать все исключения библиотеки и в текущий момент, и в будущем:
# client.ру
import mathlib try:
mathlib.func(...) except mathlib.NumErr:
...сообщить и выполнить восстановление...
Когда вы снова займетесь обновлением своего кода, то можете добавлять новые исключения как новые подклассы общего суперкласса:
# mathlib.ру
class Uflow(NumErr): pass
В результате пользовательский код, который перехватывает исключения вашей библиотеки, останется работоспособным без внесения изменений. На самом деле впоследствии вы вольны добавлять, удалять и модифицировать исключения произвольным образом — до тех пор, пока клиенты указывают имя суперкласса, а он остается незатронутым, они защищены от изменения в вашем наборе исключений. Другими словами, исключения на основе классов обеспечивают лучший ответ на проблемы при сопровождении, чем могли бы строковые исключения.
Иерархии исключений, основанные на классах, также поддерживают предохранение состояния и наследование способами, которые делают их идеальными в более крупных программах. Тем не менее, чтобы понять их роли, сначала понадобится взглянуть, как классы исключений, определяемые пользователем, связаны со встроенными исключениями, от которых они унаследованы.
Встроенные классы исключений
Вообще говоря, примеры из предыдущего раздела вовсе не были взяты с потолка. Все встроенные исключения, которые способен генерировать сам интерпретатор Python, представляют собой заранее определенные объекты классов. Вдобавок они организованы в неглубокую иерархию с универсальными категориями суперклассов и специфическими типами подклассов во многом подобно дереву классов исключений в предыдущем разделе.
В Python З.Х все хорошо известные исключения, которые вы видели (скажем, SyntaxError) в действительности являются лишь предварительно определенными классами, доступными в виде встроенных имен в модуле под названием builtins. В Python 2.Х они находятся в_built in_и также реализованы как атрибуты стандартного библиотечного модуля exceptions. В дополнение Python организует встроенные исключения в иерархию с целью поддержки разнообразных режимов перехвата. Ниже описаны примеры.
BaseException: самый верхний корень со стандартным методом вывода и конструктором
Корневой суперкласс верхнего уровня для исключений. Этот класс не предназначен для прямого наследования классами, определяемыми пользователем (взамен используйте Exception). Он обеспечивает стандартное поведение вывода и предохранения состояния, наследуемое подклассами. Если вызвать встроенную функцию с экземпляром данного класса (например, через print), тогда он возвратит отображаемые строки аргументов конструктора, которые были переданы при создании экземпляра (или пустую строку в отсутствие аргументов). Вдобавок, если только подклассы не заместили конструктор класса BaseException, то все аргументы, переданные конструктору класса во время создания экземпляра, сохраняются в атрибуте args в форме кортежа.
Exception: корень определяемых пользователем исключений
Корневой суперкласс верхнего уровня для исключений, связанных с приложением. Это непосредственный подкласс BaseException и суперкласс для каждого второго встроенного исключения кроме классов событий выхода в систему (SystemExit, Keyboardlnterrupt и GeneratorExit). От него, а не от BaseException, должны наследоваться почти все классы исключений, определяемые пользователем. Когда такое соглашение соблюдается, то указание Exception в обработчике оператора try гарантирует, что программа будет перехватывать все кроме событий выхода в систему, которым обычно должно быть разрешено проходить. По существу Exception становится универсальной ловушкой в операторах try и обеспечивает более высокую точность, чем пустая конструкция except.
ArithmeticError: корень численных ошибок
Подкласс Exception и суперкласс для всех численных ошибок. Его подклассы идентифицируют специфические численные ошибки: Overf lowError, ZeroDivisionError и FloatingPointError.
LookupError: корень ошибок индексирования
Подкласс Exception и суперкласс категории для ошибок индексирования в последовательностях и отображениях (IndexError и KeyError), а также некоторых ошибок при поиске Unicode.
И так далее — поскольку набор встроенных исключений подвержен частым изменениям, он полностью не документируется в книге. Дополнительные сведения ищите в справочниках вроде Python Pocket Reference ( catalog/9780596158088) или в руководстве по библиотекам Python. На самом деле дерево встроенных классов исключений в линейках Python З.Х и Python 2.Х слегка отличается в аспектах, которые мы здесь опускаем, т.к. они не имеют отношения к примерам.
В Python 2.Х вы также можете увидеть дерево встроенных классов исключений в справочном тексте по модулю exceptions (функция help обсуждалась в главах 4 и 15):
>>> import exceptions
»> help (exceptions)
...справочный текст не показан...
Модуль был удален в Python З.Х, где актуальная справка доступна на других ресурсах.
Категории встроенных исключений
Дерево встроенных классов исключений позволяет выбирать, насколько конкретными или универсальными будут обработчики. Например, поскольку встроенное исключение ArithmeticError является суперклассом для более специфических исключений, таких как Overf lowError и ZeroDivisionError:
• указывая в try исключение ArithmeticError, вы будете перехватывать возникшую численную ошибку любого вида;
• указывая в try исключение ZeroDivisionError, вы будете перехватывать только ошибку этого конкретного типа, но не другие.
Аналогично из-за того, что Exception в Python З.Х представляет собой суперкласс для всех исключений уровня приложения, в большинстве случаев вы можете применять его как универсальную ловушку. Эффект во многом похож на пустую конструкцию except, но исключениям выхода в систему разрешено проходить и распространятся надлежащим образом:
try:
action()
except Exception: # Исключения выхода в систему здесь
# не перехватываются ...обработать все исключения приложения...
else:
...обработать случай отсутствия исключений...
Однако в Python 2.Х это работает не совсем универсально, потому что автономные определяемые пользователем исключения, реализованные в виде классических классов, не обязаны быть подклассами корневого класса Exception. Такая методика более надежна в Python З.Х, т.к. там требуется, чтобы все классы были производными от встроенных классов исключений. Тем не менее, как обсуждалось в предыдущей главе, даже в Python З.Х данная схема сопряжена с большинством тех же самых потенциальных ловушек, что и пустая конструкция except — она может перехватывать исключе-
ния, предназначенные для других мест, и способна маскировать подлинные ошибки в программе. Поскольку проблема настолько распространена, мы снова вернемся к ней при описании затруднений в следующей главе.
Независимо от того, будете вы задействовать категории в дереве встроенных классов исключений или нет, они служат хорошим примером; за счет использования похожих методик для исключений на основе классов в собственном коде вы можете предоставлять наборы исключений, отличающиеся гибкостью и легкостью модификации.
g версии Python 33 были переделаны иерархии встроенных исключений ввода; вывода и операционной системы. Добавились новые специфические классы | исключений, соответствующие распространенным номерам файловых и
1 системных ошибок, которые вместе с остальными классами исключений,
относящимися к вызовам операционной системы, собраны под суперклассом категории OSError. Первые из упомянутых имен исключений сохранены для обратной совместимости.
Ранее программы инспектировали данные, присоединенные к экземпляру исключения, чтобы выяснить, какая конкретно ошибка произошла, и возможно повторно генерировали остальные с целью дальнейшего распространения (модуль errno для удобства содержит имена, предварительно установленные в коды ошибок, а номер ошибки доступен в обобщенном кортеже как V. args [ 0 ] и в атрибуте V. errno):
с:\temp> ру -3.2 >>> try:
... f = open(*nonesuch.txt')
. . . except IOError as V:
# Или errno.N, V.args[0]
# Такой файл отсутствует
# Распространить остальные
... if V.errno == 2:
... print('No such file')
... else:
... raise
No such file
Код по-прежнему работает в Python 3.3, но с новыми классами программы на Python 3.3 и последующих версиях могут быть более конкретными в отношении исключений, которые они намерены обрабатывать, и игнорировать остальные:
с: \temp> ру -3.3 >>> try:
... f = open('nonesuch.txt')
. . . except FileNotFoundError:
. . . print ('No such file') # Такой файл отсутствует
No such file
Более полное описание данного расширения и его классов ищите на других ресурсах.
Стандартный вывод и состояние
Встроенные исключения также обеспечивают вывод стандартного отображения и предохранение состояния, что зачастую и есть тем объемом логики, который требуется классам, определяемым пользователем. Если только вы не переопределили в своих классах конструкторы, унаследованные от встроенных классов исключений, тогда любые передаваемые аргументы автоматически сохраняются в кортежном атрибуте args экземпляра и автоматически отображаются при выводе экземпляра. Пустые кортеж и отображаемая строка применяются, когда аргументы конструктора не передавались, а одиночный аргумент отображается сам по себе (не как кортеж).
Это объясняет, почему аргументы, переданные встроенным классам исключений, появляются в сообщениях об ошибках — любые аргументы конструктора присоединяются к экземпляру и отображаются при выводе экземпляра:
»> raise IndexError # То же, что и IndexError () : без аргументов
Traceback (most recent call last) :
File "<stdin>", line 1, in <module>
IndexError
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>”, строка 1, в <модуль>
Ошибка индекса
»> raise IndexError (' spam') # Аргумент конструктора присоединяется и выводится Traceback (most recent call last) :
File ,,<stdin>", line 1, in <module>
IndexError: spam
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 1, в <модуль>
Ошибка индекса: spam
» I = IndexError (' spam’) # Аргумент доступен в атрибуте объекта
»> I.args ('spam',)
»> print (I) # Атрибуты отображаются при выводе вручную
spam
То же самое остается справедливым для определяемых пользователем исключений в Python З.Х (и для классов нового стиля в Python 2.Х), потому что они наследуют методы конструктора и отображения, присутствующие в их встроенных суперклассах:
»> class Е (Exception) : pass
»> raise Е
Traceback (most recent call last) :
File ,,<stdin>”, line 1, in <module>
_main_.E
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 1, в <модуль>
_main_.Е
»> raise E('spam')
Traceback (most recent call last):
File "<stdin>n, line 1, in <module>
_main_.E: spam
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 1, в <модуль>
_main_.Е: spam
»> I = Е (1 spam1)
»> I.args
(1 spam',)
>>> print(I)
spam
В случае перехвата в операторе try объект экземпляра исключения предоставляет доступ к первоначальным аргументам конструктора и методу отображения:
>>> try:
... raise Е('spam')
. . . except Е as X:
. . . print(X) # Отображает и сохраняет аргументы конструктора
... print(X.args)
... print(repr(X))
spam
('spam',)
E('spam',)
>>> try: # Множество аргументов сохраняются/отображаются в виде кортежа
... raise Е('spam', 'eggs', 'ham')
. . . except E as X:
... print('%s %s' % (X, X.args))
('spam', 'eggs', 'ham') ('spam', 'eggs', 'ham')
Обратите внимание, что объекты экземпляров исключений сами не являются строками, но используют протокол перегрузки операций_str_, исследованный в главе
30, чтобы предоставить отображаемые строки при выводе; для их конкатенации с настоящими строками выполняйте ручные преобразования: str (X) + 1astr1, ? %s' % X и т.п.
Хотя такая автоматическая поддержка состояния и отображения полезна сама по себе, для удовлетворения специфических потребностей в отображении и предохранении состояния вы всегда можете переопределить такие унаследованные методы, как _str_и_init_, в подклассах Exception — чему и посвящен следующий раздел.
Специальное отображение при выводе
Как было показано в предыдущем разделе, по умолчанию экземпляры исключений, основанных на классах, отображают все, что было передано конструктору класса, когда они перехвачены и выводятся:
>>> class MyBad (Exception) : pass »> try:
. . . raise MyBad ('Sorry—my mistake! ') # Сожалею--моя ошибка!
. . . except MyBad as X:
... print(X)
Sorry--my mistake!
Такая унаследованная стандартная модель отображения также применяется, если исключение отображается как часть сообщения об ошибке, когда оно не перехвачено:
>>> raise MyBad(' Sorry--my mistake! ')
Traceback (most recent call last) :
File "<stdin>", line 1, in <module>
_main_.MyBad: Sorry—my mistake!
Трассировка (самый последний вызов указан последним) :
Файл ”<stdin>", строка 1, в <модуль>
_main_.MyBad: Sorry--my mistake!
Для многих ролей этого достаточно. Но чтобы обеспечить более специализированное отображение, вы можете определить в своем классе один из двух методов перегрузки строкового представления (_герг_или_str_), возвращая строку, которую желательно отображать для исключения. Возвращаемая методом строка будет отображаться, если исключение либо перехвачено и выведено, либо достигло стандартного обработчика:
»> class MyBad (Exception) :
. . . def_str_(self) :
. . . return 'Always look on the bright side of life. . . 1
# Всегда смотри на светлую сторону жизни. . .
»> try:
... raise MyBad()
. . . except MyBad as X:
... print(X)
Always look on the bright side of life. . .
>>> raise MyBad ()
Traceback (most recent call last) :
File "<stdin>", line 1, in <module>
_main_.MyBad: Always look on the bright side of life...
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 1, в <модуль>
_main_.MyBad: Always look on the bright side of life. . .
Все, что ваш метод возвращает, помещается в сообщения об ошибках для непере-хваченных исключений и используется, когда исключения выводятся явно. В целях иллюстрации метод здесь возвращает жестко закодированную строку, но он может также выполнять произвольную текстовую обработку, скажем, применяя присоединенную к объекту экземпляра информацию состояния. Мы взглянем на варианты информации состояния в следующем разделе.
Здесь есть одна тонкость: обычно для целей отображения исключения ! вам придется переопределять метод_str_, поскольку встроенные суперклассы исключений уже его имеют, и в ряде контекстов_str_пред-
^ ^ почтительнее_герг_— в том числе и при отображении сообщений об
ошибках. Если вы определите метод_герг_, то вывод благополучно
вызовет взамен метод_str_встроенного суперкласса!
>>> class Е(Exception):
def_repr_(self) : return 'Not called! ' # He вызывается!
»> raise E('spam’)
_main_.E: spam
>» class E (Exception) :
def _str_(self): return 'Called!1 # Вызывается!
»> raise E(,spam')
_main_.E: Called!
Дополнительные сведения об этих специальных методах перегрузки операций ищите в главе 30.
Специальные данные и поведение
Помимо поддержки гибких иерархий классы исключений также предоставляют хранилище для добавочной информации состояния в форме атрибутов экземпляра. Как выяснилось ранее, встроенные суперклассы исключений предлагают стандартный конструктор, который автоматически сохраняет свои аргументы в кортежном атрибуте экземпляра по имени args. Несмотря на то что стандартный конструктор подходит во многих ситуациях, для более специализированных нужд мы можем предоставить собственный конструктор. В дополнение классы могут определять методы для использования в обработчиках, которые снабжают нас заранее реализованной логикой обработки исключений.
Предоставление деталей исключения
Когда исключение сгенерировано, оно способно пересекать границы произвольных файлов — оператор raise, генерирующий исключение, и перехватывающий его оператор try могут располагаться в совершенно разных файлах модулей. Хранить дополнительные детали в глобальных переменных обычно нереально, т.к. оператор может не знать, в каком файле находятся эти глобальные переменные. Передача добавочной информации состояния наряду с самим исключением позволяет оператору try получать доступ к ним более надежно.
Благодаря классам все достигается почти автоматически. Мы видели, что при генерации исключения Python передает объект экземпляра класса вместе с исключением. Код в операторах try способен обращаться к сгенерированному экземпляру, указывая дополнительную переменную после ключевого слова as в обработчике except, что обеспечивает естественную привязку для снабжения обработчика данными и поведением.
Например, программа разбора файлов данных может сигнализировать об ошибке формата путем генерации экземпляра исключения, который заполняется добавочными деталями об ошибке:
»> class FormatError (Exception) :
def_init_(self, line, file) :
self, line = line self .file = file
»> def parser () :
raise FormatError(42, file='spam.txt') # Когда обнаружена ошибка
»> try:
. .. parser()
. . . except Forma tError as X:
... print ('Error at: %s %s' % (X.file, X.line))
Error at: spam.txt 42
Здесь в конструкции except переменной X присваивается ссылка на экземпляр, который был создан, когда возникло исключение. Это дает доступ к атрибутам, присоединенным к экземпляру специальным конструктором. Хотя мы могли бы полагаться на стандартное предохранение состояния встроенными суперклассами, оно менее свойственно нашему приложению (и не поддерживает ключевые аргументы, применяемые в предыдущем примере):
>>> class FormatError(Exception): pass # Унаследованный конструктор »> def parser () :
raise FormatError (42, 'spam, txt’) # Ключевые аргументы не разрешены! >» try:
. . . parser ()
. . . except FormatError as X:
... print('Error at:', X.args[0], X.args[l]) # He специфично для
# данного приложения
Error at: 42 spam.txt
Предоставление методов исключений
Кроме разрешения специфичной для приложения информации состояния специальные конструкторы также лучше поддерживают добавочные линии поведения для объектов исключений. То есть в классе исключения могут также определяться методы, подлежащие вызову в обработчике. Скажем, в приведенном ниже коде из файла excparse.py добавлен метод, который использует информацию состояния исключения для автоматической регистрации ошибок в файле:
from_future_ import print_function # Совместимость с Python 2.X
class FormatError(Exception): logfile = ’formaterror.txt’
def _init_(self, line, file) :
self.line = line self.file = file def logerror(self) :
log = open(self.logfile, 'a')
print(’Error at:', self.file, self.line, file=log) def parser () :
raise FormatError(40, 'spam.txt')
if _name_ == '_main_' :
try:
parser() except FormatError as exc: exc.logerror()
После запуска сценарий записывает свои сообщения об ошибках в файл, реагируя на вызовы методов внутри обработчика исключений:
c:\code> del formaterror.txt c:\code> py -3 excparse.py c:\code> py -2 excparse.py c:\code> type formaterror.txt
Error at: spam.txt 40 Error at: spam.txt 40
В таком классе методы (вроде logerror) могут также быть унаследованы из суперклассов, а атрибуты экземпляра (наподобие line и file) предоставляют место для сохранения информации состояния, которая предлагает дополнительное содержимое для применения в более поздних вызовах методов. Кроме того, классы исключений вольны настраивать и расширять унаследованное поведение:
class CustomFormatError(FormatError): def logerror(self):
...что-то уникальное... raise CustomFormatError(...)
Другими словами, поскольку они определены с помощью классов, все преимущества ООП, которые обсуждались в части Part VI, доступны для использования с исключениями в Python.
Два финальных замечания: во-первых, сгенерированный объект экземпляра, присвоенный ехс в этом коде, также доступен обобщенно как второй элемент в результирующем кортеже вызова sys. exc infо () — инструмента, который возвращает информацию о самом недавнем исключении. Такой интерфейс должен применяться, если вы не указываете имя исключения в конструкции except, но все равно нуждаетесь в доступе к возникшему исключению или присоединенной информации состояния либо методам. Во-вторых, хотя метод logerror нашего класса добавляет специальное сообщение в журнальный файл, он мог бы также генерировать стандартное сообщение об ошибке Python с трассировкой стека, используя инструменты из стандартного библиотечного модуля traceback, который работает с объектами трассировки.
Рассмотрение sys. excinf о и объектов трассировки продолжается в следующей главе.
Резюме
В главе демонстрировалась методика создания определяемых пользователем исключений. Вы узнали, что начиная с версий Python 2.6 and 3.0, исключения реализуются в виде объектов экземпляров классов (предшествующая альтернативная модель исключений на основе строк была доступна в более ранних версиях, но теперь объявлена устаревшей). Классы исключений поддерживают концепцию иерархий исключений, которая облегчает сопровождение, позволяет присоединять к исключениям данные и поведение как атрибуты и методы экземпляров, а также разрешает исключениям наследовать данные и поведение от суперклассов.
Как выяснилось, указание суперкласса в операторе try обеспечивает перехват этого класса и всех его подклассов ниже в дереве классов. Суперклассы становятся названиями категорий исключений, а подклассы — более конкретными типами исключений внутри таких категорий. Также было показано, что встроенные суперклассы исключений, от которых должны наследоваться подклассы, предоставляют удобные стандартные методы для вывода и предохранения состояния, в случае необходимости допускающие переопределение.
Следующая глава завершает очередную часть книги исследованием ряда сценариев применения исключений и обзором инструментов, часто используемых программистами на Python. Но прежде чем переходить к ее чтению, ответьте на контрольные вопросы главы, чтобы закрепить полученные знания.
Проверьте свои знания: контрольные вопросы
1. Какие два новых ограничения накладываются на определяемые пользователем исключения в Python З.Х?
2. Как сгенерированные исключения, основанные на классах, сопоставляются с обработчиками?
3. Назовите два способа присоединения информации контекста к объектам исключений.
4. Назовите два способа указания текста сообщений об ошибках для объектов исключений.
5. Почему исключения на основе строк в наше время больше не должны использоваться?
Проверьте свои знания: ответы
1. В Python З.Х исключения должны определяться посредством классов (т.е. генерируется и перехватывается объект экземпляра класса). Вдобавок классы исключений обязаны быть производными от встроенного класса BaseException; в большинстве программ они наследуются от его подкласса Exception, чтобы поддерживать универсальные обработчики для нормальных разновидностей исключений.
2. Исключения на основе классов сопоставляются по отношениям с суперклассами: указание суперкласса в обработчике исключений приводит к перехвату экземпляров данного класса и экземпляров всех его подклассов ниже в дереве классов. По этой причине вы можете думать о суперклассах как об общих категориях исключений, а о подклассах как о более специфичных типах исключений внутри таких категорий.
3. Вы можете присоединять информацию контекста к основанным на классах исключениям путем заполнения атрибутов экземпляра в сгенерированном объекте экземпляра обычно внутри специального конструктора класса. Для более простых потребностей встроенные суперклассы исключений предлагают конструктор, который автоматически сохраняет свои аргументы в экземпляре (в форме кортежа в атрибуте args). В обработчиках исключений вы указываете переменную, которой должен присваиваться сгенерированный экземпляр, после чего с помощью этого имени получаете доступ к присоединенной информации состояния и вызываете любые методы, определенные в классе.
4. Текст сообщений об ошибках в исключениях на основе классов может указываться посредством специального метода перегрузки операций_str_. Для более простых потребностей встроенные суперклассы исключений автоматически отображают все, что передается конструкторам классов. Операции вроде print и str автоматически извлекают отображаемую строку объекта исключения, когда он выводится либо явно, либо как часть сообщения об ошибке.
5. Потому что гак сказал Гвидо — они были удалены, начиная с версий Python 2.6 и 3.0. Вероятно, на то имелись веские причины: исключения на основе строк не поддерживали категории, информацию состояния и наследование поведения, как это делают исключения, основанные на классах. По существу исключения на основе строк было легче использовать на первых порах, пока программы имели небольшие размеры, но по мере роста программ применять такие исключения становилось все труднее.
6. С требованием того, чтобы исключения были классами, связаны также недостатки: нарушение работы существующего кода и создание заблаговременной зависимости от знаний — новички обязаны сначала изучить классы и ООП, прежде чем они сумеют реализовывать новые исключения или даже по-настоящему понимать исключения вообще. Фактически именно потому эта относительно прямолинейная тема была отложена вплоть до настоящего места книги. Так или иначе, зависимости подобного рода — далеко не редкость в современном языке Python.
ГЛАВА 36
Назад: Детали обработки исключений
Дальше: Проектирование с использованием исключений