Проектирование с использованием исключений
Настоящая глава завершает эту часть книги совокупностью тем, касающихся проектирования с применением исключений, и набором распространенных сценариев использования, после чего обсуждаются затруднения и предлагаются упражнения. Поскольку глава заканчивает порцию книги, в целом посвященную фундаментальным основам, в ней также приводится краткий обзор инструментов для разработки, чтобы помочь вам перейти из разряда новичков в языке Python в разработчики прикладных приложений на Python.
Вложение обработчиков исключений
До сих пор в большинстве примеров для перехвата исключений применялся только одиночный оператор try, но что произойдет, если один try физически вложить внутрь другого try? Вдобавок что означает, когда в try вызывается функция, которая запускает еще один try? Формально операторы try можно вкладывать с точки зрения как синтаксиса, так и потока управления на протяжении выполнения кода. Ранее я кратко об этом упоминал, но давайте проясним саму идею.
Оба сценария можно понять, если осознавать, что во время выполнения интерпретатор Python укладывает операторы try в стопку. При генерации исключения Python возвращается к самому последнему введенному оператору try с дающей совпадение конструкцией except. Из-за того, что каждый оператор try оставляет маркер, интерпретатор Python может переходить обратно на более ранние операторы try за счет инспектирования уложенных в стопку маркеров. Именно такое вложение активных обработчиков подразумевается, когда речь идет о распространении исключений вверх до “более высоких” обработчиков — обработчики подобного рода являются просто операторами try, введенными раньше в потоке выполнения программы.
На рис. 36,1 показано, что происходит при вложении операторов try с конструкциями except во время выполнения. Объем кода в блоке try может быть значительным, и он способен содержать вызовы функций, которые обращаются к другому коду, отслеживающему те же самые исключения. Когда в конечном итоге генерируется исключение, Python переходит обратно на самый последний введенный оператор try, в котором указано сгенерированное исключение, выполняет конструкцию except данного оператора и затем возобновляет выполнение после этого try.
► def funcl(): try:func2()-except E:
► def func2():
Рис. 36.1. Вложенные операторы try/except: когда генерируется исключение (вами либо интерпретатором Python), управление передается самому последнему введенному оператору try с конструкций except, дающей совпадение, и программа возобновляет выполнение после этого оператора try. Конструкции except перехватывают и прекращают распространение исключения — именно в них производится обработка и восстановление после исключений
После того, как исключение перехвачено, время его существования заканчивается — управление не передается всем подходящим операторам try, в которых указано имя исключения; шанс его обработать предоставляется только первому (т.е. самому недавнему) try. Например, на рис. 36.1 оператор raise в функции func2 возвращает управление обработчику в fund и затем программа продолжает выполнение внутри fund.
Напротив, когда операторы try, содержащие только конструкции finally, оказываются вложенными, то при возникновении исключения каждый блок finally выполняется по очереди — интерпретатор Python продолжает распространение исключения выше до других операторов try и со временем возможно до стандартного обработчика верхнего уровня (выводящего стандартное сообщение об ошибке). Как иллюстрируется на рис. 36.2, конструкции finally не уничтожают исключение — они просто указывают код, подлежащий выполнению при выходе из каждого try в течение процесса распространения исключений. Если в момент генерации исключения есть много активных конструкций try/finally, тогда все они будут выполнены при условии, что исключение где-то по пути не перехватывается каким-то оператором try/except.
Другими словами, то, куда управление в программе переходит в случае генерации исключения, зависит целиком от того, где оно находилось — это работа потока управления во время выполнения сценария, а не только синтаксиса. По существу распространение исключения продолжается обратно сквозь время до операторов try, вход в которые происходил, но выход пока нет. Такое распространение останавливается, как только поток управления раскручивается до дающей совпадение конструкции except, но не так, как оно проходит через конструкции finally на своем пути.
Пример: вложение в потоке управления
Чтобы прояснить эту концепцию вложения, давайте рассмотрим пример. В файле модуля nestexc.py, код которого представлен ниже, определяются две функции. Функция action2 реализована для генерации исключения (складывать числа и последовательности нельзя), а функция actionl содержит внутри вызов action2 в обработчике try, чтобы перехватывать исключение:
def action2 () :
print (1 + []) # Генерирует TypeError
def actionl (): try:
action2()
except TypeError: # Самый последний совпадающий try
print (’inner try') # внутренний оператор try
try:
actionl()
# Здесь только в случае повторной
# генерации исключения в actionl
# внешний оператор try
except TypeError:
print('outer try')
% python nestexc.py
inner try
Однако обратите внимание, что код верхнего уровня модуля в конце файла тоже помещает вызов actionl внутрь обработчика try. Когда action2 сгенерирует исключение TypeError, появятся два активных оператора try — один в actionl и один на верхнем уровне файла модуля. Интерпретатор Python выберет и выполнит самый недавний try с совпадающей конструкцией except, которым в данном случае будет try внутри actionl.
Опять-таки место, куда попадает исключение, зависит от того, как поток управления проходит через программу во время выполнения. По указанной причине, чтобы узнать, где вы окажетесь, нужно знать, где вы были. В приведенной ситуации место обработки исключения определяется в большей степени потоком управления, нежели синтаксисом операторов. Тем не менее, мы можем вкладывать обработчики исключений также и синтаксически — эквивалентный сценарий описан в следующем разделе.
Пример: синтаксическое вложение
Как упоминалось при рассмотрении нового объединенного оператора tr У/ except/finally в главе 34, операторы try допускается вкладывать синтаксически за счет их позиции в исходном коде:
try: try:
action2()
except TypeError:
print('inner try’) except TypeError:
print('outer try')
# Самый последний совпадающий try
# Здесь только в случае повторной генерации
# исключения во вложенном обработчике
Однако на самом деле в коде всего лишь настраивается та же самая структура вложения обработчиков, что и в предыдущем примере (и ведущая себя идентично). В действительности синтаксическое вложение работает в точности как случаи, изображенные на рис. 36.1 и 36.2. Единственная разница в том, что вложенные обработчики физически внедрены в блок try, а не реализованы где-то в функциях, которые вызываются из блока try. Скажем, при исключении запускаются все вложенные обработчики finally, вложены они синтаксически или посредством прохождения потока управления через физически разделенные части кода:
>>> try:
. . . try:
. . . raise IndexError
... finally:
... print('spam')
. . . finally:
... print('SPAM')
spam
SPAM
Traceback (most recent call last) :
File "<stdin>", line 3, in <module>
IndexError
Трассировка (самый последний вызов указан последним) :
Файл "<stdin>", строка 3, в <модуль>
Ошибка индекса
Графическая иллюстрация работы такого кода приводилась на рис. 36.2; результат тот же, но логика функции здесь была записана как вложенные операторы. Более полезный пример синтаксического вложения можно найти в файле except-finally.ру:
def raisel(): raise IndexError
def noraise (): return
def raise2(): raise SyntaxError
for func in (raisel, noraise, raise2) :
print('<%s>' I func. name_)
try:
try:
func() except IndexError:
print('caught IndexError') finally:
print('finally run') print('...')
Код перехватывает исключение, если оно сгенерировано, и выполняет действие завершения finally независимо от того, возникало ли исключение. Его понимание может потребовать какого-то времени, но эффект будет таким же, как при объединении конструкций except и finally в единственном операторе try в Python 2.5 и последующих версиях:
% python except-finally .py
<raisel>
caught IndexError finally run
<noraise> finally run
<raise2> finally run
Traceback (most recent call last) :
File "except-finally.py", line 9, in <module> func()
File "except-finally.py", line 3, in raise2 def raise2(): raise SyntaxError SyntaxError: None
Трассировка (самый последний вызов указан последним) :
Файл "except-finally .ру", строка 9, в <модуль> func ()
Файл "except-finally .ру", строка 3, в <модуль> def raise2(): raise SyntaxError Ошибка индекса: None
Как было показано в главе 34, начиная с версии Python 2.5, конструкции except и finally можно смешивать в одном операторе try. Наряду с поддержкой множества конструкций except это делает часть описанного выше синтаксического вложения ненужным, хотя эквивалентное вложение времени выполнения распространено в более крупных программах на Python, Кроме того, синтаксическое вложение по-прежнему работает в наши дни, все еще может встречаться в коде, написанном до выхода версии Python 2.5, способно делать разрозненные роли except и finally более явными и может использоваться в качестве методики для реализации альтернативных линий поведения при обработке исключений.
Идиомы исключений
Итак, мы ознакомились с механизмом, лежащим в основе исключений. Теперь давайте рассмотрим ряд других способов его типичного применения.
Прерывание множества вложенных циклов: "безусловный переход"
Как упоминалось в начале этой части книги, исключения часто могут использоваться для исполнения тех же ролей, что и операторы “безусловного перехода” в других языках, чтобы реализовывать более произвольные передачи управления. Тем не менее, исключения предоставляют структурированный способ, который локализует переход к специфическому блоку вложенного кода.
В данной роли оператор raise похож на “безусловный переход”, а конструкции except и имена исключений занимают место программных меток. Таким способом можно выходить только из кода, помещенного внутрь try, но возможность очень важна — по-настоящему произвольные операторы “безусловного перехода” способны сделать код чрезвычайно сложным для понимания и сопровождения.
Например, оператор break языка Python выходит только из одиночного ближайшего объемлющего цикла, но мы всегда можем применить исключения, чтобы в случае необходимости прервать более одного вложенного цикла:
>>> class Exitloop(Exception): pass »> try:
. .. while True:
. . . while True:
. . . for i in range (10) :
... if i > 3: raise Exitloop # break выходит только
# из одного уровня вложенности
... print(11оорЗ: %sf % i)
. . . print(11оор2')
... print('loopl1)
. . . except Exitloop:
. . . print (1 continuingf) # Или просто pass, чтобы двигаться дальше
1оорЗ: 0 1оорЗ: 1 1оорЗ: 2 1оорЗ: 3 continuing »> i
4
Если вы измените raise на break, то получите бесконечный цикл, т.к. произойдет выход только из наиболее глубоко вложенного цикла for и попадание в цикл второго уровня вложенности. Код затем выведет 1оор2 и начнет цикл for заново.
Вдобавок обратите внимание, что переменная i остается такой же, какой она была после выхода из оператора try. Присваивания переменных, выполненные внутри try, в общем случае не отменяются, хотя переменные экземпляров исключений, указанные в заголовках конструкций except, локализуются в пределах этих конструкций, а локальные переменные любых функций, которые завершились из-за raise, отбрасываются. Формально локальные переменные активных функций выталкиваются из стека вызовов и в результате объекты, на которые они ссылаются, могут быть подвергнуты сборке мусора, но такой шаг выполняется автоматически.
Исключения не всегда являются ошибками
В Python все ошибки являются исключениями, но не все исключения — ошибками. Скажем, в главе 9 было показано, что методы чтения файловых объектов по достижении конца файла возвращают пустую строку. Напротив, встроенная функция input (которую мы впервые встретили в главе 3, задействовали в интерактивном цикле в главе 10 и узнали, что в Python 2.Х она называется rawinput) при каждом вызове читает строку текста из стандартного входного потока sys . stdin и при достижении конца файла генерирует встроенное исключение EOFError.
В отличие от файловых методов функция input не возвращает пустую строку — пустая строка из input означает пустую строку в файле. Однако, несмотря на свое имя, исключение EOFError в данном контексте представляет собой просто сигнал, не ошибку. По причине такого поведения, если признак конца файла не должен завершать работу сценария, то вызов input часто встречается помещенным внутрь обработчика try и вложенным в цикл, как в следующем коде:
while True: try:
line = input () # Чтение строки из stdin (raw_input в Python 2.X)
except EOFError:
break # Выход из цикла по достижении конца файла
else:
...обработка очередной строки...
Несколько других встроенных исключений аналогичным образом являются сигналами, а не ошибками — например, вызов sys . exit () и нажатие <Ctrl+C> на клавиатуре генерируют соответственно исключения SystemExit и Keyboardlnterrupt.
В Python также имеется набор встроенных исключений, которые вместо ошибок представляют предупреждения; ряд из них служит для того, чтобы сигнализировать об использовании устаревших (постепенно исчезающих) языковых средств. За дополнительной информацией о встроенных исключениях обращайтесь в руководство по стандартной библиотеке, а сведения об исключениях, генерируемых как предупреждения, ищите в документации по модулю warnings.
Функции могут сигнализировать об условиях с помощью raise
Определяемые пользователем исключения способны также сигнализировать о неошибочных условиях. Скажем, процедура поиска может быть реализована так, чтобы в случае нахождения совпадения генерировать исключение, а не возвращать флаг состояния для интерпретации в вызывающем коде. Ниже обработчик исключения выполняет работу оператора if/else по проверке возвращаемого значения:
class Found(Exception): pass
def searcher(): if ... успех...:
raise Found () # Генерировать исключения вместо возвращения флагов
else:
return
try:
searcher()
# Исключение, если элемент был найден
# иначе элемент не был найден
except Found:
. . .успех..
else:
. ..неудача
В более общем смысле такая кодовая структура также может быть полезной для любой функции, которая не способна возвращать индикаторное значение, обозначающее успех или неудачу. Например, если в широко применяемой функции все объекты являются допустимыми возвращаемыми значениями, то сигнализировать об условии неудачи с помощью любого возвращаемого значения попросту невозможно. Исключения предлагают способ предупреждения о результатах без возвращаемого значения:
class Failure(Exception): pass
def searcher (): if ...успех...:
return ...найденный элемент... else:
raise Failure ()
try:
item = searcher() except Failure:
...элемент не найден... else:
...использовать элемент...
Поскольку язык Python глубоко динамически типизированный и полиморфный, исключения в отличие от индикаторных возвращаемых значений являются предпочитаемым способом сигнализации о таких условиях.
Закрытие файлов и серверных подключений
Примеры такой категории встречались в главе 34. Тем не менее, в качестве резюме отметим, что инструменты обработки исключений также часто используются для обеспечения финализации системных ресурсов независимо от того, возникала ошибка во время обработки или нет.
Скажем, некоторые серверы требуют закрытия подключений для завершения сеанса. Аналогично выходные файлы могут требовать закрытия, чтобы сбросить свои буферы на диск для ожидающих клиентов, а входные файлы могут потреблять файловые ресурсы, не будучи закрытыми. Хотя при сборке мусора файловые объекты автоматически закрываются, если все еще открыты, в некоторых реализациях Python иногда нелегко выяснить, когда сборка мусора произойдет.
В главе 34 было показано, что наиболее универсальный и явный способ гарантировать выполнение завершающих действий для индивидуального блока кода предусматривает применение оператора try/finally:
myfile = open(г'С:\code\textdata', 'w') try:
...обработка myfile... finally:
myfile.close()
Также выяснилось, что в Python 2.6, 3.0 и последующих версиях некоторые объекты потенциально облегчают выполнение завершающих действий, предоставляя диспетчеры контекстов, которые автоматически завершают работу или закрывают объекты в случае использования с оператором with/as:
with open (г1С:\code\textdata', 'w') as myfile:
...обработка myfile...
Так какой вариант лучше? Как обычно, все зависит от ваших программ. По сравнению с традиционным оператором try/finally диспетчеры контекстов менее явные, что противоречит общей философии проектирования в Python. Диспетчеры контекстов также вероятно менее универсальны — они доступны только для избранных объектов, а реализация определяемых пользователем диспетчеров контекстов для учета общих требований к завершению сложнее написания операторов try/ finally.
С другой стороны, применение диспетчеров контекстов требует меньше кода, чем использование try/finally, как демонстрировалось в предшествующих примерах. Вдобавок к действиям выхода протокол диспетчеров контекстов поддерживает действия входа. В действительности они могут сберечь строку кода, когда никакие исключения не ожидаются (хотя за счет дальнейшего вложения и добавления отступов к логике обработки файлов):
myfile = open(filename, 'w') # Традиционная форма
...обработка myfile...
myfile.close ()
with open (filename) as myfile: # Форма с диспетчером контекста . ..обработка myfile...
Однако неявная обработка исключений оператора with делает его более сопоставимым с явной обработкой исключений try/finally. Несмотря на то что try/finally является шире применимой методикой, диспетчеры контекстов могут оказаться предпочтительнее, где они уже доступны либо их добавочная сложность оправдана.
Отладка с помощью внешних операторов try
Вы также можете использовать обработчики исключений для замещения стандартного поведения обработки исключений на верхнем уровне Python. Поместив целую программу (или обращение к ней) во внешний оператор try в своем коде верхнего уровня, вы можете перехватывать любое исключение, которое произошло во время выполнения программы, посредством чего отменяя стандартное завершение работы программы.
Ниже пустая конструкция except перехватывает любое неперехваченное исключение, сгенерированное во время выполнения программы. Чтобы получить действительное исключение, которое произошло в таком режиме, извлеките результат вызова функции sys.exc info из встроенного модуля sys; она возвращает кортеж, первый элемент которого содержит класс текущего исключения и сгенерированный объект экземпляра (вскоре функция sys .exc info будет описана более детально):
try:
...запуск программы...
except: # Сюда поступают все неперехваченные исключения
import sys
print('uncaught!', sys.exc_info()[0], sys.exc_info()[1])
Такая структура обычно применяется на этапе разработки для сохранения программ в активном состоянии даже после возникновения ошибок — внутри цикла она позволяет прогонять дополнительные тесты без необходимости в перезапуске. Прием также используется при тестировании кода других программ, как объясняется в следующем разделе.
| В качестве стороннего замечания: чтобы узнать больше об обработке за-j вершения программ без восстановления после них, ознакомьтесь также
j со стандартным библиотечным модулем Python под названием atexit.
^ Настроить работу обработчика исключений верхнего уровня возможно и с помощью функции sys . except hook. Эти и другие связанные инструменты описаны в руководстве по библиотеке Python.
Выполнение внутрипроцессных тестов
Ряд только что рассмотренных кодовых схем можно комбинировать в испытательном приложении, которое тестирует другой код внутри того же процесса. Следующий неполный код демонстрирует общую модель:
import sys
log = open('testlog', 'a')
from testapi import moreTests, runNextTest, testName
def testdriver():
while moreTests () :
try:
runNextTest() except:
print(1 FAILED1, testName(), sys.exc_info() [: 2], file=log) else:
print (' PASSED', testName(), file=log)
testdriver{)
Функция testdriver выполняет в цикле серию обращений к тестам (в примере мы абстрагируемся от модуля testapi). Поскольку неперехваченное исключение в тестовом сценарии нормально прекратило бы работу испытательного приложения, вам необходимо поместить обращения к тестовым сценариям внутрь оператора try, если вы хотите продолжать процесс тестирования после неудачного прохождения теста. Пустая конструкция except обычным образом перехватывает любые неперехвачен-ные исключения, сгенерированные тестовым сценарием, и применяет sys. excinf о для регистрации исключения в журнальном файле. Конструкция else выполняется, когда никаких исключений не было — успешное прохождение теста.
Такой стереотипный код типичен для систем, которые тестируют функции, модули и классы, запуская их в том же самом процессе, где выполняется испытательное приложение. Тем не менее, на практике тестирование может оказаться гораздо более сложным, чем проиллюстрированное выше. Например, для тестирования внешних программ вы могли бы взамен проверять коды состояния или вывод, порожденный такими инструментами запуска программ, как os . system и os .рореп, которые использовались ранее в книге и раскрываются в руководстве по стандартной библиотеке. Инструменты подобного рода обычно не генерируют исключения для ошибок во внешних программах — на самом деле тестовые сценарии могут выполняться параллельно с испытательным приложением.
В конце главы будут кратко представлены более полные фреймворки тестирования, входящие в состав Python, вроде doctest и PyUnit, которые предлагают инструменты для сравнения ожидаемого вывода с фактическими результатами.
Дополнительные сведения о функции sys. exc_infо
Результат вызова sys. excinf о, который применялся в предшествующих двух разделах, дает возможность обработчику исключений получать доступ к самому последнему сгенерированному исключению обобщенным образом. Это особенно полезно при использовании пустой конструкции except для слепого перехвата всех исключений, чтобы выяснить, какое исключение было сгенерировано:
try:
except:
# sys.exc_info()[0:2] представляет класс и экземпляр исключения
Если никакие исключения не обрабатывались, тогда такой вызов возвращает кортеж с тремя значениями None. В противном случае значениями будут (type, value, traceback):
• type — класс обрабатываемого исключения;
• value — экземпляр класса исключения, который был сгенерирован;
• traceback — объект трассировки, который представляет стек вызовов в месте, где первоначально возникло исключение, и применяется модулем traceback для генерации сообщений об ошибках.
Мы видели в предыдущей главе, что функция sys .exc info иногда может быть полезной для установления конкретного типа исключения при перехвате суперклассов категорий исключений. Однако поскольку в такой ситуации получить тип исключения можно также за счет извлечения атрибута_class_экземпляра, полученного
через as, вызов sys.exc info часто избыточен кроме случая с пустой конструкцией except:
try:
except General as instance:
# instance._class_ представляет класс исключения
Как уже было показано, использование здесь Exception для имени исключения General аналогично пустой конструкции except будет перехватывать все исключения, отличающиеся от событий выхода в систему, но менее экстремально, и по-прежнему предоставлять доступ к экземпляру исключения и его классу. Тем не менее, применение интерфейсов объекта экземпляра и полиморфизма зачастую является более эффективным подходом, чем проверка типов исключений — методы исключений могут определяться для каждого класса и выполняться обобщенным образом:
try:
except General as instance:
# instance.method() делает то, что нужно, для данного экземпляра
Как обычно, слишком большая конкретика в Python способна ограничить гибкость вашего кода. Полиморфный подход наподобие того, что использовался в последнем примере, в целом лучше поддерживает будущее развитие, нежели явные проверки и действия, специфичные к типам.
Отображение сообщений об ошибках и трассировок
Наконец, объект трассировки исключения, доступный в результате вызова sys.exc info из предыдущего раздела, также применяется стандартным библиотечным модулем traceback для формирования стандартного сообщения об ошибке и отображения стека вручную. Файл модуля traceback содержит несколько интерфейсов, поддерживающих обширную настройку, раскрыть полностью которые здесь невозможно из-за нехватки места, но сами основы просты. Взгляните на содержимое подходящим образом названного файла, badly.ру:
import traceback
def inverse(x): return 1 / x
try:
inverse(0)
except Exception:
traceback.print_exc(file=open('badly.exc', 'w'))
print('Bye')
В коде используется удобная функция print exc из модуля traceback, которая по умолчанию потребляет данные sys . excinf о; после запуска сценарий выводит сообщение об ошибке в файл, что удобно в тестовых программах, которым необходимо перехватывать ошибки, но вдобавок полноценно их регистрировать:
с: \code> python badly.py
Bye
c:\code> type badly.exc
Traceback (most recent call last) :
File "C:\Code\badly.py", line 7, in <module> inverse(0)
File "C:\Code\badly.py", line 4, in inverse return 1 / x
ZeroDivisionError: division by zero
Трассировка (самый последний вызов указан последним) :
Файл "C:\Code\badly.py", строка 1, в <модуль> inverse(0)
Файл "С: \Code\badly.ру", строка 4, в inverse return 1 / х
Ошибка деления на ноль; деление на ноль
Более подробные сведения об объектах трассировки, модуле traceback, в котором они применяются, и связанных темах ищите в других справочных ресурсах и руководствах.
^ Примечание, касающееся нестыковки версий. Более старые инструменты
| sys . exctype и sys . excvalue по-прежнему работают в Python 2.Х для
! извлечения типа и значения самого последнего исключения, но они могут
управлять только одним глобальным исключением для целого процесса. В Python З.Х упомянутые два имени были удалены. Более новый и предпочтительный вызов sys . excinf о (), доступный в Python 2.Х и Python З.Х, взамен отслеживает информацию об исключении каждого потока и потому специфичен к потокам. Разумеется, такое отличие важно только в случае использования множества потоков в программах на Python (тема, выходящая за рамки настоящей книги), но в Python З.Х проблема усиливается. За дополнительными сведениями обращайтесь на другие ресурсы.
Советы по проектированию с использованием исключений и связанные с ними затруднения
В этой главе я собрал вместе советы по проектированию и затруднения, т.к. выясняется, что наиболее распространенные затруднения в значительной степени уходят своими корнями в проблемы, касающиеся проектирования. В общем и целом применять исключения в Python несложно. Подлинное искусство связано с принятием решения о том, до какой степени конкретными или универсальными должны быть конструкции except, и сколько кода подлежит помещению внутрь операторов try. Давайте займемся сначала вторым вопросом.
Что должно быть помещено внутрь операторов try?
В принципе вы могли бы поместить каждый оператор в своем сценарии внутрь собственного оператора try, но поступать так попросту нелепо (тогда и операторы try пришлось бы помещать внутрь операторов try!). То, что помещается внутрь try, в действительности представляет собой задачу проектирования, которая выходит за рамки самого языка, и станет более очевидным в ходе работы. А пока ниже приведено несколько эмпирических правил.
• Операции, которые обычно терпят неудачу, как правило, должны помещаться внутрь операторов try. Например, операции, взаимодействующие с системным состоянием (открытия файлов, обращения к сокетам и т.п.) будут главными кандидатами для try.
• Однако существуют отклонения от предыдущего правила — в простом сценарии у вас может возникнуть желание, чтобы неудачи таких операций уничтожали вашу программу, а не были перехвачены и проигнорированы. Сказанное особенно справедливо, если неудача является устойчивой ошибкой. Неудачи в Python обычно приводят к выводу полезных сообщений об ошибках (не к полным отказам) и являются наилучшим исходом, на который могли бы рассчитывать некоторые программы.
• Действия по завершении должны быть реализованы в операторах try/finally, чтобы гарантировать их выполнение, если только не оказывается доступным диспетчер контекста как вариант with/as. Форма оператора try/ finally позволяет запускать код независимо от того, возникали исключения в произвольных сценариях или нет.
• Иногда удобнее помещать вызов крупной функции внутрь единственного оператора try, не засоряя саму функцию множеством операторов try. В итоге все исключения из функции проникают в try вокруг вызова, а объем кода внутри функции сокращается.
Реализуемые типы программ, вероятно, также будут оказывать влияние на объем кода обработки исключений. Например, серверы обычно обязаны функционировать постоянно и потому, скорее всего, потребуют наличия операторов try для перехвата и восстановления после исключений. Программы внутрипроцессного тестирования показанного в главе вида возможно тоже будут обрабатывать исключения. Тем не менее, простые одноразовые сценарии часто полностью игнорируют обработку исключений, поскольку неудача на любом шаге требует прекращения работы сценария.
Перехват слишком многого: избегайте использования пустой конструкции
except и конструкции except Exception
Как уже упоминалось, универсальность обработчиков исключений — определяющий проектный выбор. Python позволяет быть разборчивым в том, какие исключения перехватывать, но иногда необходимо соблюдать осторожность, чтобы обработчики не становились излишне инклюзивными. Например, вы видели, что пустая конструкция except перехватывает любое исключение, которое может сгенерироваться на стадии выполнения кода в блоке try.
Писать такой код легко и временами желательно, но вы можете также в итоге перехватить ошибку, которая ожидается обработчиком try выше в структуре вложения исключений. Скажем, обработчик исключений вроде следующего перехватывает и прекращает любое исключение, которое его достигает, невзирая на то, что оно ожидается еще одним обработчиком:
def func(): try:
... # Здесь генерируется исключение IndexError
except:
... # Но все поступает сюда и исчезает!
try:
func()
except IndexError: # Исключение IndexError должно обрабатываться здесь
Вероятно хуже то, что такой код может также перехватывать не имеющие к нему отношения системные исключения. В Python исключения генерируют даже такие вещи, как ошибки памяти, подлинные дефекты в программе, остановки итерации, прерывания с клавиатуры и выходы в систему. Если только вы не разрабатываете отладчик или похожий инструмент, то исключения подобного рода обычно не должны перехватываться в вашем коде.
Например, сценарии обычно завершают работу, когда управление оказывается за концом файла верхнего уровня. Однако Python также предоставляет встроенный вызов sys. exit (код-состояния), чтобы сделать возможным ранее завершение. В действительности он работает путем генерации встроенного исключения SystemExit с целью окончания программы, так что обработчики try/finally запускаются при выходе и программы специальных видов способны перехватывать это событие По указанной причине оператор try с пустой конструкцией except может неосознанно помешать критически важному выходу, как показано в следующем файле (exiter .ру):
import sys def bye():
sys.exit (40) # Критическая ошибка: прекратить прямо сейчас!
try:
bye () except:
print (' got it' ) # Ox, мы проигнорировали выход
print('continuing...') # продолжение...
% python exiter.py
got it
continuing...
Вы просто можете не ожидать всех видов исключений, которые могли бы возникнуть в течение выполнения операции. В этом конкретном случае может помочь применение встроенных классов исключений из предыдущей главы, потому что суперкласс Exception не является суперклассом класса SystemExit:
try:
bye ()
except Exception: # He перехватывает выходы, но _будет_
# перехватывать многие другие исключения
Тем не менее, в других случаях такая схема не лучше пустой конструкции except — из-за того, что Exception представляет собой суперкласс выше всех встроенных исключений кроме событий выхода в систему, он по-прежнему обладает потенциалом перехвата исключений, предназначенных для других мест в программе.
Пожалуй, хуже всего то, что использование пустой конструкции except и перехват суперкласса Exception также будут перехватывать подлинные ошибки в программе, которым должно быть разрешено проходить большую часть времени. Фактически эти две методики способны по существу отключить механизм сообщения об ошибках интерпретатора Python, затрудняя обнаружение просчетов в коде. Взгляните на показанный далее код:
mydictionary = {...} try:
# Ох, опечатка
# Предположим, мы получили KeyError
х = myditctionary['spam'] except:
x = None . . .продолжить работу с x. . .
Программист здесь предполагает, что единственным видом ошибок, которые могут произойти при индексировании словаря, будет отсутствующий ключ. Но поскольку имя myditctionary написано неправильно (должно быть mydictionary), интерпретатор Python взамен генерирует исключение NameError для ссылки на неопределенное имя, которое обработчик молча перехватывает и игнорирует. При доступе к словарю обработчик исключений будет некорректно заполнять переменную стандартным значением, маскируя ошибку в программе.
Более того, перехват Exception здесь не поможет — он будет иметь в точности тот же эффект, что и пустая конструкция except, благополучно и молча заполняя переменную стандартным значением и маскируя ошибку в программе, о которой вы наверняка хотели бы знать. Если подобное происходит в коде, который далек от того места, где применяются извлеченные значения, тогда вам предстоит очень интересная задача отладки!
В качестве эмпирического правила: будьте в своих обработчиках как можно более конкретными — пустые конструкции except и перехват суперкласса Exception удобны, но потенциально подвержены ошибкам. Скажем, в последнем примере было бы лучше записать except KeyError:, чтобы сделать свои намерения явными и избежать перехвата событий, не имеющих отношения к делу. В более простых сценариях вероятность возникновения проблем может оказаться не настолько значительной, чтобы перевесить удобство перехвата всех исключений, но в целом универсальные обработчики, как правило, являются источником неприятностей.
Перехват чересчур малого: используйте категории на основе классов
С другой стороны, обработчики также не должны быть слишком конкретными. Указывая специфичные исключения в try, вы перехватываете только то, что фактически перечислили. Это необязательно плохо, но если система развивается и в будущем должна перехватывать другие исключения, тогда вам может понадобиться добавлять их в списки исключений повсюду в коде.
Мы сталкивались с таким явлением в предыдущей главе. Например, приведенный ниже обработчик реализован так, чтобы трактовать MyExceptl и MyExcept2 как нормальные случаи, а все остальное как ошибку. Однако если в будущем добавится исключение MyExcept3, то оно будет обрабатываться как ошибка, пока вы не обновите список исключений:
try:
except (MyExceptl, MyExcept2) : # Работа нарушится, если позже добавится MyExcept3 ... # Не ошибки
else:
... # Предполагается, что это ошибка
К счастью, аккуратное применение исключений на основе классов, которые обсуждались в главе 34, способно полностью устранить эту ловушку при сопровождении кода. Если вы перехватываете общий суперкласс, то в будущем можете добавлять и генерировать более специфичные подклассы без необходимости в ручном расширении списков исключений в конструкциях except — суперкласс становится расширяемой категорией исключений:
try:
except SuccessCategoryName: # Продолжит нормально работать, если позже
# добавится подкласс МуЕхсерЬЗ ... # Не ошибки
else:
... # Предполагается, что это ошибка
Другими словами, даже небольшое проектное решение проходит длинный путь. Мораль истории в том, чтобы соблюдать осторожность, не делая обработчики исключений ни слишком универсальными, ни чересчур конкретными, и разумно подбирать степень детализации операторов try. Политики в отношении исключений должны быть частью полного проектного решения, особенно в более крупных системах.
Сводка по базовому языку
Примите поздравления! На этом ознакомление с основами языка программирования Python завершено. Если вы зашли настолько далеко, то уже стали полноценным программистом на Python. В части, посвященной более сложным темам, рассматриваются дополнительные темы, которые вскоре будут описаны. Тем не менее, с точки зрения основ история о Python и главное путешествие в данной книге окончены.
В ходе чтения вы ознакомились почти со всем, что можно увидеть в самом языке, причем достаточно глубоко, чтобы применить полученные знания к большинству кода, с которым вы, вполне вероятно, столкнетесь в “диком” мире открытого кода. Вы изучили встроенные типы, операторы и выражения, а также инструменты, используемые для построения более крупных программных единиц — функций, модулей и классов. Вы исследовали важные вопросы проектирования программного обеспечения, полную парадигму ООП, инструменты функционального программирования, концепции архитектур программ, компромиссы, связанные с альтернативными инструментами, и многое другое, формируя набор навыков, который теперь можно свободно применять при разработке реальных приложений.
Комплект инструментов Python
С этого момента ваша будущая карьера, связанная с Python, будет по большому счету заключаться в овладении комплектом инструментов, доступным для программирования прикладных приложений на Python. Вы обнаружите, что это непрерывная задача. Скажем, стандартная библиотека содержит сотни модулей, а общедоступное программное обеспечение предлагает еще больше инструментов. Можно потратить десятилетия в достижении мастерства использования всех имеющихся инструментов, особенно с учетом постоянного появления новых инструментов, ориентированных на новые технологии (поверьте мне — я свыше 25 лет в данной сфере!).
Вообще говоря, Python предоставляет следующие комплекты инструментов.
Встроенные типы
Встроенные типы наподобие строк, списков и словарей позволяют быстро писать простые программы.
Расширения Python
Для более сложных задач вы можете расширять Python за счет написания собственных функций, модулей и классов.
Скомпилированные расширения
Хотя данную тему мы в книге не раскрываем, Python также можно расширять с помощью модулей, написанных на внешних языках вроде С или C++.
Поскольку Python организует свои комплекты инструментов по уровням, вы можете принимать решение о том, насколько глубоко программы должны погружаться в эту иерархию для любой задачи — применять встроенные типы для простых сценариев, добавлять расширения на Python для более крупных систем и использовать скомпилированные расширения для сложных работ. В книге были раскрыты только первые две из указанных категорий, но их достаточно, чтобы начать заниматься действительным программированием на Python.
Помимо прочих существуют инструменты, ресурсы или прецеденты применения Python практически в любой вычислительной области, какую только можно вообразить. Подсказки о том, куда двигаться дальше, были даны в обзоре приложений и пользователей Python в главе 1 первого тома. Вероятно, вы обнаружите, что с помощью такого мощного языка, как Python, решать распространенные задачи часто гораздо легче и даже приятнее, чем можно было ожидать.
Инструменты для разработки, ориентированные на более крупные проекты
Большинство примеров в книге были довольно небольшими и самодостаточными. Так было задумано, чтобы помочь вам освоить основы. Но теперь, когда вы знаете все о базовом языке, пришло время приступить к изучению способов использования встроенных и сторонних интерфейсов Python для выполнения реальной работы.
На практике программы на Python могут становиться значительно крупнее примеров, с которыми вы экспериментировали до сих пор в книге. Даже в Python наличие в нетривиальных и полезных программах тысяч строк кода — далеко не редкость после того, как вы соберете все индивидуальные модули в систему. Хотя основные инструменты структурирования программ на Python, подобные модулям и классам, оказывают большую помощь в управлении такой сложностью, временами дополнительную поддержку могут предложить и другие инструменты.
Для разработки крупных систем вы обнаружите такую поддержку как в Python, так и в общедоступной области. Вы видели некоторые из них в действии, и я упоминал о нескольких других. Чтобы помочь вам с дальнейшими шагами, ниже представлен краткий тур и сводка по наиболее распространенным инструментам такого рода.
PyDoc и строки документации
Функция help из PyDoc и HTML-интерфейсы были введены в главе 15 первого тома. PyDoc предлагает систему документации для ваших модулей и объектов, интегрируется с синтаксисом строк документации Python и является стандартной частью системы Python. Советы по источникам документации приводились в главах 15 и 4 первого тома.
PyChecker и PyLint
Поскольку Python настолько динамический язык, сообщения о некоторых ошибках в программе не появятся до тех пор, пока программа не будет запущена (даже синтаксические ошибки не улавливаются до запуска либо импортирования файла). Такую особенность нельзя считать крупным недостатком — как и с большинством языков, это всего лишь означает необходимость тестирования кода на Python перед его поставкой. В худшем случае в Python вы по существу меняете стадию компиляции на начальную стадию тестирования. Кроме того, динамическая природа Python, автоматически выдаваемые сообщения об ошибках и модель исключений позволяют найти и исправить ошибки легче и быстрее, чем в ряде других языков. Например, в отличие от С, когда встречаются ошибки, интерпретатор Python не терпит полный отказ.
Однако инструменты могут помочь и здесь. Системы PyChecker и PyLint обеспечивают поддержку для выявления часто встречающихся ошибок заблаговременно, до запуска сценария. Их роли подобны ролям программы lint при разработке на языке С. Некоторые разработчики на Python прогоняют свой код через PyChecker перед его тестированием или поставкой, чтобы отловить любые возможные скрытые проблемы. На самом деле первоначально имеет смысл опробовать указанные системы — ряд предупреждений, выдаваемых этими инструментами, могут помочь вам научиться выявлять и избегать распространенных недоразумений при программировании на Python. Системы PyChecker и PyLint являются сторонними пакетами с открытым кодом, доступными на вебсайте PyPI или через предпочитаемый поисковый механизм в веб-сети. Они также могут иметь вид IDE-сред с графическими пользовательскими интерфейсами.
PyUnit (он же иnit test)
В главе 25 первого тома было показано, как добавлять в файл Python код самотестирования за счет применения приема с_name_== '_main_’ в конце
файла — простого протокола модульного тестирования. Для достижения более сложных целей тестирования в Python имеются два инструмента поддержки тестирования. Первый, PyUnit (называемый unittest в руководстве по библиотеке), предоставляет объектно-ориентированный фреймворк, основанный на классах, для указания и настройки тестовых сценариев и ожидаемых результатов. Он имитирует фреймворк JUnit для Java. Это сложно устроенная система модульного тестирования на основе классов, которая подробно описана в руководстве по библиотеке Python.
doctest
Стандартный библиотечный модуль doctest предлагает второй и более простой подход к регрессионному тестированию, базирующийся на средстве строк документации Python. Грубо говоря, для использования doctest вы вырезаете и вставляете содержимое журнала интерактивного сеанса тестирования в строки документации файлов исходного кода. Затем doctest извлекает ваши строки документации, разбивает их на тестовые сценарии и результаты и повторно прогоняет тесты для сличения их результатов с ожидаемыми. Деятельность модуля doctest можно настраивать разнообразными способами; полные сведения
о модуле doctest ищите в руководстве по библиотеке.
ЮЕгСреды
Мы обсуждали IDE-среды для Python в главе 3 первого тома. Такие IDE-среды, как IDLE, предлагают графическую среду для редактирования, выполнения, отладки и просмотра программ на Python. Ряд продвинутых IDE-сред наподобие Eclipse, Komodo, NetBeans и других перечисленных в главе 3 первого тома могут поддерживать дополнительные задачи, включая интеграцию с системой контроля версий исходного кода, рефакторинг кода, инструменты управления проектами и т.д. За более полной информацией обращайтесь в главу 3 первого тома.
Профилировщики
Из-за того, что Python является настолько высокоуровневым и динамическим, ожидания относительно производительности, основанные на опыте работе с другими языками, обычно неприменимы к коду на Python. Чтобы по-настоящему выделить в своем коде узкие места в плане производительности, понадобится добавить логику измерения времени с помощью инструментов из модуля time или timeit либо запустить код под управлением модуля profile. Примеры использования модулей для измерения времени при сравнении скорости выполнения итерационных инструментов и версий Python приводились в главе 21 первого тома.
Профилирование обычно будет первым шагом при оптимизации — кода для ясности, далее профиля для выявления узких мест и времени выполнения альтернативных версий для медленных частей программы. Для второго шага profile является стандартным библиотечным модулем, который реализует профилировщик исходного кода на Python. Он запускает предоставленную строку кода (например, импортирование файла сценария или вызов функции), после чего по умолчанию выводит в стандартный выходной поток отчет, который содержит статистические данные о производительности — количество обращений к каждой функции, время выполнения каждой функции и другую информацию.
Модуль profile можно запускать в виде сценария либо импортировать, а также настраивать различными путями; скажем, он способен сохранять статистику выполнения в файле, чтобы проанализировать ее позже с помощью модуля pstats. Для профилирования в интерактивном режиме импортируйте модуль profile и вызовите profile. run (' код’), передав подлежащий профилированию код в форме строки (например, вызов функции, импортирование файла или код, читаемый из файла). Для профилирования в командной строке системной оболочки используйте команду вида python -m profile main.py аргументы (формат более подробно объясняется в приложении А). В руководствах по стандартной библиотеке Python описаны другие варианты профилирования; скажем, модуль cProfile имеет интерфейсы, идентичные profile, но сопряжен с меньшими накладными расходами, поэтому может лучше подойти для профилирования долго выполняющихся программ.
Отладчики
Варианты отладки обсуждались также в главе 3 первого тома (см. врезку “Отладка кода Python”). В качестве обзора отметим, что большинство IDE-сред для разработки на Python поддерживают отладку, основанную на графическом пользовательском интерфейсе, а стандартная библиотека Python дополнительно включает модуль для отладки исходного кода под названием pdb. Модуль pdb предоставляет интерфейс командной строки и работает во многом подобно распространенным отладчикам для языка С (например, dbx, gdb).
Как и в случае с профилировщиком, отладчик pdb можно запускать либо интерактивно, либо из командной строки, а также импортировать и обращаться к нему в программе на Python. Для его применения в интерактивном режиме импортируйте модуль, начните выполнение кода, вызвав функцию pdb (скажем, pdb. run ('main ()’)), и вводите команды отладки в интерактивной подсказке pdb. Для запуска pdb в командной строке системной оболочки используйте команду вида python -m pdb main.py аргументы. Модуль pdb также включает полезную функцию послеаварийного анализа pdb. pm (), которая запускает отладчик после того, как встретилось исключение, возможно в сочетании с флагом -i интерпретатора Python. Дополнительные сведения о таких инструментах приведены в приложении А.
Поскольку IDE-среды вроде IDLE также предлагают интерфейсы отладки в стиле “указать и щелкнуть”, в наши дни pdb не считается важным инструментом кроме ситуаций, когда графический пользовательский интерфейс недоступен или желателен больший контроль. За советами по использованию графического пользовательского интерфейса IDE-среды IDLE обращайтесь в главу 3 первого тома. По правде говоря, pdb и IDE-среды не особенно часто применяются на практике — как отмечалось в главе 3, большинство программистов либо вставляют операторы print, либо просто читают сообщения об ошибках интерпретатора Python: вероятно не самый высокотехнологичный подход, но достаточно практичный, чтобы одержать победу в мире Python!
Варианты поставки
В главе 2 первого тома были представлены распространенные инструменты для упаковки программ на Python. Системы ру2ехе, Pylnstaller и другие перечисленные в главе способны упаковывать байт-код и виртуальную машину Python в “фиксированные двоичные” автономные исполняемые файлы, которые не требуют наличия установленной копии Python на целевой машине и скрывают код вашей программы. Кроме того, программы на Python могут поставляться в формах исходного кода (.ру) или байт-кода (.рус), а привязки импортирования поддерживают специальные методики упаковки, такие как автоматическое извлечение файлов . zip и шифрование байт-кода.
Мы также кратко коснулись стандартных библиотечных модулей distutils, которые предоставляют варианты упаковки для модулей и пакетов Python и расширений, написанных на С; детали приведены в руководствах по Python. Появившаяся сторонняя система упаковки Python “eggs” (формат “яиц”) предлагает еще одну альтернативу, которая также учитывает зависимости; поищите в веб-сети дополнительную информацию.
Варианты оптимизации
На тот случай, когда скорость имеет значение, имеется несколько вариантов оптимизации программ. Система РуРу, описанная в главе 2 первого тома, предоставляет оперативный компилятор для перевода байт-кода на Python в двоичный машинный код, a Shed Skin предлагает транслятор Python в C++. Иногда вы также можете встречать файлы оптимизированного байт-кода .руо, создаваемые и запускаемые с помощью флага командной строки -О интерпретатора Python, который обсуждался в главе 22 первого тома и в главе 34 и вводится в действие в главе 39. Тем не менее, из-за того, что такой прием обеспечивает весьма скромный рост производительности, обычно он не используется кроме как для удаления отладочного кода.
В качестве последнего средства для повышения производительности вы можете перенести части своей программы в компилируемые языки, такие как С. Расширения С более детально рассматриваются в книге Programming Python () и в стандартных руководствах по Python. В целом показатели скорости самого интерпретатора Python также с течением времени улучшаются, поэтому обновление до более поздних выпусков может повысить производительность.
Другие советы касательно более крупных проектов
В книге встречались разнообразные базовые языковые средства, которые также станут более полезными, как только вы приступите к реализации крупных проектов. К ним относятся пакеты модулей (глава 24 первого тома), исключения на основе классов (глава 34), псевдозакрытые атрибуты классов (глава 31), строки документации (глава 15 первого тома), файлы конфигурации путей модулей
(глава 22 первого тома), сокрытие имен от from * с помощью списков_all_
и имена в стиле X (глава 25 первого тома), добавление кода самотестирования посредством приема_name_== '_main_ (глава 25 первого тома), применение общих правил проектирования для функций и модулей (главы 17, 19 и 25 первого тома), использование паттернов объектно-ориентированного проектирования (глава 31 и другие) и т.д.
Чтобы узнать о других доступных инструментах для крупномасштабной разработки на Python, просмотрите веб-сайт PyPI и веб-сеть в целом. Применение Python на самом деле представляет собой более широкую тему, чем изучение Python, поэтому вам придется обращаться к дополнительным ресурсам.
Резюме
В данной главе часть книги, посвященная исключениям, была завершена обзором концепций проектирования, распространенных сценариев использования исключений и часто применяемых инструментов для разработки.
Кроме того, глава также завершает основной материал всей книги. К настоящему моменту вы ознакомились с тем подмножеством языка Python, которое используют большинство программистов, а возможно и больше. Фактически, если вы дочитали до этого места, то можете смело себя считать официальным программистом на Python. Обязательно подберите себе футболку или наклейку на ноутбук с соответствующим логотипом (и не забудьте внести знание Python в свое резюме).
Следующая и последняя часть книги является набором глав, посвященных темам, которые считаются сложными, но все равно относятся к категории базового языка. Все главы финальной части предназначены для дополнительного чтения, потому что не каждый программист на Python обязан углубляться в их тематику, а некоторые могут отложить изучение изложенных в них вопросов до момента, когда они понадобятся. Конечно, многие из вас могут остановиться здесь и заняться исследованием ролей Python в своих предметных областях. Откровенно говоря, на практике прикладные библиотеки оказываются более важными, чем продвинутые и в чем-то даже экзотические языковые средства.
С другой стороны, если вас заботят вещи вроде Unicode или двоичных данных, имеется потребность работать с инструментами для построения API-интерфейсов, такими как дескрипторы, декораторы и метаклассы, или просто есть желание узнать дальнейшие детали, тогда следующая часть книги поможет вам начать. Довольно крупные примеры в последней части книги также предоставят вам шанс увидеть уже изученные концепции примененными более реалистичными способами.
Так как вы добрались до конца основного материала книги, то получаете возможность слегка отдохнуть от контрольных вопросов по главе — предлагается всего лишь один вопрос. Как всегда, постарайтесь проработать упражнения для данной части, чтобы закрепить знания, приобретенные в прошедших главах; поскольку следующая часть предназначена для факультативного чтения, этот набор упражнений, завершающих часть, будет последним. Если вы хотите увидеть примеры того, как все изученное ранее объединяется вместе в реальных сценариях, взятых из распространенных приложений, тогда обязательно ознакомьтесь с “решением” упражнения 4 в приложении Г.
На тот случай, если вы закончили свое путешествие в этой книге, то все же просмотрите раздел “На бис” в конце главы 41, самой последней в книге (ради читателей, продолжающих изучение следующей части, я не буду здесь раскрывать секрет).
Проверьте свои знания: контрольные вопросы
1. (Вопрос является повторением шестого контрольного вопроса из главы 1 первого тома — видите, я же говорил, что это будет легко! :-) Почему слово “spam” обнаруживается в настолько многих примерах кода Python в книгах и веб-сети?
Проверьте свои знания: ответы
1. Потому что язык Python назван в честь британской комик-группы “Монти Пайтон” (основываясь на опросах, проводимых мною в учебных группах, это слишком хорошо оберегаемая тайна в мире Python!). Слово “spam” взято из пародии “Монти Пайтон”, снятой в кафетерии, где все позиции меню, похоже, включают “Spam”. Пару, пытающуюся заказать еду, заглушает хор викингов, поющих о мясных консервах марки “Spam”. Нет, правда. И если б я мог вставить аудиоклип этой песни здесь, то я бы...
Проверьте свои знания: упражнения для части VII
Поскольку мы добрались до конца этой части книги, самое время предложить несколько упражнений по исключениям, чтобы дать вам возможность попрактиковаться с основами. Исключения — действительно простой инструмент; если вы сумеете выполнить упражнения, то вероятно хорошо освоили область исключений. Решения упражнений приведены в приложении Г.
(Подсказка: вспомните, что все неуточненные имена обычно поступают из одной из четырех областей видимости.)
2. Объекты и списки исключений. Измените написанную в упражнении 1 функцию oops, чтобы она генерировала определенное вами исключение по имени MyError. Идентифицируйте свое исключение с помощью класса (если только вы не используете версию Python 2.5 или более раннюю, то обязаны поступить так). Затем расширьте оператор try в перехватывающей функции для перехвата этого исключения и его экземпляра в дополнение к IndexError, а также вывода перехваченного экземпляра.
3. Обработка ошибок. Напишите функцию по имени safe (func, *pargs, **kargs), которая запускает любую функцию с любым количеством позиционных и/или ключевых аргументов за счет применения заголовка произвольных аргументов
* и синтаксиса вызовов, перехватывает любое исключение, сгенерированное во время выполнения функции, и выводит исключение с использованием вызова exc info из модуля sys. Затем примените свою функцию safe для запуска функции oops из упражнения 1 или 2. Поместите функцию safe в файл модуля по имени exctools .ру и передайте ей функцию oops интерактивно. Какого вида сообщения об ошибках вы получите? Наконец, расширьте функцию safe, чтобы при возникновении ошибки она также выводила трассировку стека Python путем вызова встроенной функции print exc из стандартного модуля traceback; детали применения ищите ранее в этой главе и в справочном руководстве по библиотеке Python. Вероятно, мы могли бы реализовать safe как декоратор функции, используя методики из главы 32, но чтобы полностью изучить, каким образом, придется перейти к следующей части книги (за предварительным обзором обращайтесь к решениям упражнений).
4. Примеры для самообучения. В конце приложения Г я привожу несколько примеров сценариев, которые разработаны как групповые упражнения в существующих классах Python для изучения вами и самостоятельного запуска в сочетании со стандартным набором руководств по Python. Они не описаны и применяют инструменты из стандартной библиотеки Python, которые вы должны исследовать самостоятельно. Тем не менее, для многих читателей это помогает увидеть, как обсуждаемые в книге концепции объединяются в реальных программах. Если они еще больше разожгут у вас интерес, то вы можете найти множество более крупных и реалистичных примеров программ на Python прикладного уровня в книгах наподобие Programming Python ( в веб-сети.
ЧАСТЬ VIII
Более сложные темы
ГЛАВА 37