справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Глава 5 . Структура программы и управление потоком выполнения В этой главе подробно рассматриваются структура программы и способы управления потоком выполнения. В число рассматриваемых тем входят: условные операторы, итерации, исключения и менеджеры контекста. Структура программы и ее выполнение Программы на языке Python представляют собой последовательности ин- Python представляют собой последовательности ин- представляют собой последовательности ин- струкций. Все функциональные возможности языка, включая присваива- ние значений переменным, определение функций и классов, импортирова- ние модулей, реализованы в виде инструкций, обладающих равным поло- жением со всеми остальными инструкциями. В действительности в языке Python нет «специальных» инструкций и всякая инструкция может быть помещена в любом месте в программе. Например, в следующем фрагменте определяются две различные версии функции: if debug: def square(x): if not isinstance(x,float): raise TypeError(“Expected a float”) return x * x else: def square(x): return x * x Загружая исходные файлы программы, интерпретатор всегда последова- тельно выполняет все инструкции, пока не встретит конец файла. Эта мо- дель выполнения в равной степени относится как к главному файлу про- граммы, так и к библиотечным файлам модулей, загружаемым с помощью инструкции import. Выполнение по условию 117 Выполнение по условию Инструкции if, else и elif позволяют организовать выполнение программ- ного кода в зависимости от условий. В общем случае условная инструкция имеет следующий вид: if выражение: инструкции elif выражение: инструкции elif выражение: инструкции ... else: инструкции Если при несоблюдении условия никаких действий не должно выполнять- ся, можно опустить части else и elif условной инструкции. Если действия не должны выполняться при соблюдении определенного условия, можно использовать инструкцию pass: if выражение: pass # Ничего не делает else: инструкции Циклы и итерации Циклы в языке Python оформляются с помощью инструкций for и while. Например: while выражение: инструкции ёё for i in s: инструкции Инструкция while выполняет инструкции, пока условное выражение не вернет значение False. Инструкция for выполняет итерации по всем эле- ментам s, пока не исчерпаются доступные элементы. Инструкция for мо- жет работать с любыми объектами, поддерживающими итерации. К числу этих объектов относятся встроенные типы последовательностей, такие как списки, кортежи и строки, а также любые другие объекты, реализующие протокол итераторов. Считается, что объект s поддерживает итерации, если он может исполь- зоваться в следующем программном коде, который является отражением реализации инструкции for: it = s.__iter__() # Получить итератор для объекта s while 1: try: i = it.next() # Получить следующий элемент (__next__ в Python 3) except StopIteration: # Нет больше элементов 118 Глава 5. Структура программы и управление потоком выполнения break # Выполнить операции над i ... Переменная i в инструкции for i in s называется переменной цикла. На каждой итерации она принимает новое значение из объекта s. Область ви- димости переменной цикла не ограничивается инструкцией for. Если перед инструкцией for была объявлена переменная с тем же именем, ее предыду- щее значение будет затерто. Более того, по окончании цикла эта перемен- ная будет хранить последнее значение, полученное в цикле. Если элементы, извлекаемые в процессе итерации, являются последова- тельностями одного и того же размера, их можно распаковать в несколько переменных цикла, используя прием, демонстрируемый ниже: for x,y,z in s: инструкции В этом примере объект s должен содержать или воспроизводить последо- вательности, каждая из которых содержит по три элемента. В каждой ите- рации переменным x, y и z будут присваиваться значения элементов соот- ветствующей последовательности. Несмотря на то что наиболее часто этот прием используется, когда объект s является последовательностью корте- жей, тем не менее он может применяться к элементам объекта s, являю- щимся последовательностями любого типа, включая списки, генераторы и строки. При выполнении итераций, помимо самих данных, иногда бывает удобно иметь доступ к порядковому номеру итерации, как показано ниже: i = 0 for x in s: инструкции i += 1 Для этих целей в языке Python может использоваться встроенная функция enumerate() , например: for i,x in enumerate(s): инструкции Вызов enumerate(s) создает итератор, который просто возвращает последо- вательность кортежей (0, s[0]), (1, s[1]), (2, s[2]) и так далее. Другая типичная проблема, связанная с организацией циклов, – парал- лельная обработка двух или более последовательностей. Например, цикл, в каждой итерации которого требуется одновременно извлекать элементы из разных последовательностей, может быть реализован так: # s и t – это две последовательности i = 0 while i < len(s) and i < len(t): x = s[i] # Извлечь элемент из последовательности s y = t[i] # Извлечь элемент из последовательности t инструкции i += 1 Циклы и итерации 119 Э ту реализацию можно упростить, если воспользоваться функцией zip(). Например: # s и t – это две последовательности for x,y in zip(s,t): инструкции Функция zip(s,t) объединит последовательности s и t в последовательность кортежей (s[0],t[0]), (s[1],t[1]), (s[2], t[2]) и так далее, остановившись по- сле исчерпания элементов в самой короткой из последовательностей, если их длины не равны. Следует особо отметить, что в версии Python 2 функция zip() извлекает все элементы из последовательностей s и t и создает список кортежей. Для генераторов и последовательностей, содержащих огромные количества элементов, такое поведение может оказаться нежелательным. Эту проблему можно решить с помощью функции itertools.izip(), которая достигает того же эффекта, что и функция zip(), но вместо того чтобы соз- давать огромный список кортежей, она воспроизводит по одному комплек- ту объединенных значений при каждом обращении. В Python 3 функция zip() реализована точно так же. Прервать цикл можно с помощью инструкции break. Например, в следую- щем фрагменте выполняется чтение строк из текстового файла, пока не бу- дет встречена пустая строка: for line in open(“foo.txt”): stripped = line.strip() if not stripped: break # Пустая строка, прервать чтение # Обработать строку stripped ... Перейти к следующей итерации (пропустив оставшиеся инструкции в теле цикла) можно с помощью инструкции continue. Эта инструкция использу- ется не часто, но иногда она может быть полезна, – когда оформление еще одного уровня вложенности программного кода в условной инструкции могло бы привести к нежелательному усложнению программы. Например, следующий цикл пропускает все пустые строки в файле: for line in open(“foo.txt”): stripped = line.strip() if not stripped: continue # Пропустить пустую строку # Обработать строку stripped ... Инструкции break и continue воздействуют только на самый внутренний цикл. Если необходимо организовать выход из глубоко вложенной иерар- хии циклов, можно воспользоваться исключением. В языке Python отсут- Python отсут- отсут- ствует инструкция «goto». К инструкции цикла можно также присоединить инструкцию else, как по- казано в следующем примере: # for-else for line in open(“foo.txt”): 120 Глава 5. Структура программы и управление потоком выполнения stripped = line.strip() if not stripped: break # Обработать строку stripped ... else: raise RuntimeError(“Отсутствует разделитель параграфов”) Б лок else в инструкции цикла выполняется только в случае нормального завершения цикла, то есть либо когда не было выполнено ни одной итера- ции, либо после выполнения последней итерации. Если выполнение цикла прерывается преждевременно с помощью инструкции break, блок else про- пускается. Основное назначение инструкции else в циклах заключается в том, чтобы избежать установки или проверки какого-либо флага или условия, когда работа цикла прерывается преждевременно. Например, предыдущий при- мер можно переписать без использования блока else, но с применением до- полнительной переменной: found_separator = False for line in open(“foo.txt”): stripped = line.strip() if not stripped: found_separator = True break # Обработать строку stripped ... ёё if not found_separator: raise RuntimeError(“Отсутствует разделитель параграфов “) Исключения Исключения свидетельствуют об ошибках и прерывают нормальный ход выполнения программы. Исключения возбуждаются с помощью инструк- ции raise. В общем случае инструкция raise имеет следующий вид: raise Exception([value]) , где Exception – тип исключения, а value – необязательное значение с дополнительной информацией об исключении. Например: raise RuntimeError(“Неустранимая ошибка”) Если инструкция raise используется без дополнительных параметров, она повторно возбуждает последнее исключение (однако такой прием работает только в процессе обработки возникшего исключения). Перехватить исключение можно с помощью инструкций try и except, как показано ниже: try: f = open(‘foo’) except IOError as e: инструкции Исключения 121 Когда возникает исключение, интерпретатор прекращает выполнение инструкций в блоке try и отыскивает блок except, соответствующий типу возникшего исключения. В случае успеха управление передается первой инструкции в найденном блоке except. После выполнения блока except вы- полнение программы продолжается с первой инструкции, следующей за блоком try-except. В противном случае исключение передается блоку про- граммного кода, вмещающему инструкцию try. Этот программный код в свою очередь также может быть заключен в блок try-except, предусматри- вающий обработку исключения. Если исключение достигнет самого верх- него уровня программы и не будет перехвачено, интерпретатор прервет выполнение программы с сообщением об ошибке. При необходимости все неперехваченные исключения можно обработать в пользовательской функ- ции sys.excepthook(), как описывается в главе 13 «Службы Python времени выполнения». Необязательный модификатор as var в инструкции except определяет имя переменной, в которую будет записан тип возникшего исключения. Обра- ботчики исключений могут использовать это значение для получения более подробной информации об исключении. Например, проверку типа возник- шего исключения можно выполнить с помощью инструкции isinstance(). Следует отметить, что в предыдущих версиях Python инструкция except записывалась как except ExcType, var, где тип исключения и имя перемен- ной отделялись запятой (,). Этот синтаксис можно (но не рекомендуется) использовать в версии Python 2.6. В новых программах следует использо- Python 2.6. В новых программах следует использо- 2.6. В новых программах следует использо- вать синтаксис as var, потому что он является единственно допустимым в Python 3. Допускается использовать несколько блоков except для обработки разных типов исключений, как показано в следующем примере: try: выполнить некоторые действия except IOError as e: # Обработка ошибки ввода-вывода ... except TypeError as e: # Обработка ошибки типа объекта ... except NameError as e: # Обработка ошибки обращения к несуществующему имени ... Несколько типов исключений можно также обрабатывать с помощью един- ственного обработчика, например: try: выполнить некоторые действия except (IOError, TypeError, NameError) as e: # Обработать ошибку ввода-вывода, типа # или обращения к несуществующему имени ... 122 Глава 5. Структура программы и управление потоком выполнения Игнорировать исключение можно с помощью инструкции pass, как пока- зано ниже: try: выполнить некоторые действия except IOError: pass # Ничего не делать (сойдет и так). Перехватить все исключения, кроме тех, что приводят к немедленному за- вершению программы, можно, указав тип исключения Exception, как по- казано ниже: try: выполнить некоторые действия except Exception as e: error_log.write(‘Возникла ошибка: %s\n’ % e) Когда перехватываются все типы исключений, необходимо позаботиться о записи в журнал точной информации об ошибке для последующего ана- лиза. Например, в предыдущем фрагменте в журнал записывается инфор- мация, имеющая отношение к исключению. Если пренебречь этой инфор- мацией, будет очень сложно отладить программу, в которой возникают ошибки, появления которых вы не ожидаете. С помощью инструкции except, без указания типа исключения, можно пе- рехватывать исключения любых типов: try: выполнить некоторые действия except: error_log.write(‘Ошибка\n’) Правильное использование этой формы инструкции except является на- много более сложным делом, чем может показаться, поэтому лучше избе- гать такого способа обработки исключений. Например, этот фрагмент будет также перехватывать прерывания от клавиатуры и запросы на завершение программы, которые едва ли имеет смысл перехватывать. Кроме того, инструкция try может сопровождаться блоком else, который должно следовать за последним блоком except. Этот блок выполняется, если в теле инструкции try не возникло исключений. Например: try: f = open(‘foo’, ‘r’) except IOError as e: error_log.write(‘Невозможно открыть файл foo: %s\n’ % e) else: data = f.read() f.close() Инструкция finally служит для реализации завершающих действий, со- путствующих операциям, выполняемым в блоке try. Например: f = open(‘foo’,’r’) try: # Выполнить некоторые действия Исключения 123 ... finally: f.close() # Файл будет закрыт, независимо от того, что произойдет Б лок finally не используется для обработки ошибок. Он используется для реализации действий, которые должны выполняться всегда, независимо от того, возникла ошибка или нет. Если в блоке try исключений не возник- ло, блок finally будет выполнен сразу же вcлед за ним. Если возникло ис- cлед за ним. Если возникло ис- лед за ним. Если возникло ис- ключение, управление сначала будет передано первой инструкции в блоке finally , а затем это исключение будет возбуждено повторно, чтобы обеспе- чить возможность его обработки в другом обработчике. Встроенные типы исключений В табл. 5.1 перечислены встроенные типы исключений, которые определе- табл. 5.1 перечислены встроенные типы исключений, которые определе- табл. 5.1 перечислены встроенные типы исключений, которые определе- . 5.1 перечислены встроенные типы исключений, которые определе- 5.1 перечислены встроенные типы исключений, которые определе- ны в языке Python. Таблица 5.1. Встроенные типы исключений Исключение Описание BaseException Базовый класс всех исключений GeneratorExit Возбуждается методом .close() генераторов KeyboardInterrupt Возбуждается нажатием клавишей прерывания (обычно Ctrl-C) SystemExit Завершение программы Exception Базовый класс для всех исключений, не связан- ных с завершением программы StopIteration Возбуждается для прекращения итераций StandardError Базовый класс для всех встроенных исключе- ний (только в Python 2). В Python 3 – базовый класс всех исключений, наследующих класс Exception ArithmeticError Базовый класс исключений, возбуждаемых арифметическими операциями FloatingPointError Ошибка операции с плавающей точкой ZeroDivisionError Деление или деления по модулю на ноль AssertionError Возбуждается инструкциями assert AttributeError Возбуждается при обращении к несуществую- щему атрибуту EnvironmentError Ошибка, обусловленная внешними причинами IOError Ошибка ввода-вывода при работе с файлами OSError Ошибка операционной системы EOFError Возбуждается по достижении конца файла 124 Глава 5. Структура программы и управление потоком выполнения Исключение Описание ImportError Ошибка в инструкции import LookupError Ошибка обращения по индексу или ключу IndexError Ошибка обращения по индексу за пределами последовательности KeyError Ошибка обращения к несуществующему ключу словаря MemoryError Нехватка памяти NameError Не удалось отыскать локальное или глобальное имя UnboundLocalError Ошибка обращения к локальной переменной, которой еще не было присвоено значение ReferenceError Ошибка обращения к объекту, который уже был уничтожен RuntimeError Универсальное исключение NotImplementedError Обращение к нереализованному методу или функции SyntaxError Синтаксическая ошибка IndentationError Ошибка оформления отступов TabError Непоследовательное использование символа табуляции (генерируется при запуске интерпре- татора с ключом –tt) SystemError Нефатальная системная ошибка в интерпрета- торе TypeError Попытка выполнить операцию над аргументом недопустимого типа ValueError Недопустимый тип UnicodeError Ошибка при работе с символами Юникода UnicodeDecodeError Ошибка декодирования символов Юникода UnicodeEncodeError Ошибка кодирования символов Юникода UnicodeTranslateError Ошибка трансляции символов Юникода Исключения организованы в иерархию, как показано в табл. 5.1. Все ис- ключения, входящие в ту или иную группу, могут быть перехвачены при использовании имени группы в определении блока except. Например: try: инструкции except LookupError: # Перехватит исключение IndexError или KeyError инструкции Таблица 5.1 (продолжение) Исключения 125 или try: инструкции except Exception: # Перехватит любые программные исключения инструкции Наверху иерархии исключения сгруппированы в зависимости от того, свя- заны ли они с завершением программы. Например, исключения SystemExit и KeyboardInterrupt не входят в группу Exception, потому что, когда програм- мист предусматривает обработку всех программных исключений, он обыч- но не стремится перехватить завершение программы. Определение новых исключений Все встроенные исключения определяются как классы. Чтобы создать ис- ключение нового типа, нужно объявить новый класс, наследующий класс Exception , как показано ниже: class NetworkError(Exception): pass Чтобы воспользоваться новым исключением, его достаточно просто пере- дать инструкции raise, например: raise NetworkError(“Невозможно найти компьютер в сети.”) При возбуждении исключения с помощью инструкции raise допускается передавать конструктору класса дополнительные значения. В большинстве случаев это обычная строка, содержащая текст сообщения об ошибке. Од- нако пользовательские исключения могут предусматривать возможность приема одного или более значений, как показано в следующем примере: class DeviceError(Exception): def __init__(self,errno,msg): self.args = (errno, msg) self.errno = errno self.errmsg = msg ёё # Возбудить исключение (передав несколько аргументов) raise DeviceError(1, ‘Нет ответа’) При создании собственного класса исключения, переопределяющего метод __init__() , важно не забыть присвоить кортеж, содержащий аргументы ме- тода __init__(), атрибуту self.args, как было показано выше. Этот атрибут используется при выводе трассировочной информации. Если оставить этот атрибут пустым, пользователи не смогут увидеть информацию об исключе- нии, когда возникнет ошибка. Исключения могут быть организованы в иерархии с помощью механизма наследования. Например, исключение NetworkError, объявленное выше, могло бы служить базовым классом для более специфичных исключений. Например: class HostnameError(NetworkError): pass class TimeoutError(NetworkError): pass ёё 126 Глава 5. Структура программы и управление потоком выполнения def error1(): raise HostnameError(“Хост не найден”) ёё def error2(): raise TimeoutError(“Превышено время ожидания”) try: error1() except NetworkError as e: if type(e) is HostnameError: # Выполнить действия, характерные для ошибки этого типа ... В данном случае инструкция except NetworkError перехватывает все исклю- чения, наследующие класс NetworkError. Чтобы определить конкретный тип возникшей ошибки, выполняется проверка типа исключения с помо- щью функции type(). Для получения информации о последнем возникшем исключении можно также использовать функцию sys.exc_info(). Менеджеры контекста и инструкция with Надлежащее управление системными ресурсами, такими как файлы, бло- кировки и соединения, часто является достаточно сложной задачей, осо- бенно в соединении с обработкой исключений. Например, возникшее ис- ключение может заставить программу пропустить инструкции, отвечаю- щие за освобождение критических ресурсов, таких как блокировки. Инструкция with позволяет организовать выполнение последовательности инструкций внутри контекста, управляемого объектом, который играет роль менеджера контекста. Например: with open(“debuglog”,”a”) as f: f.write(“Отладка\n”) инструкции f.write(“Конец\n”) ёё import threading lock = threading.Lock() with lock: # Начало критического блока инструкции # Конец критического блока В первом примере инструкция with автоматически закроет открытый файл, когда поток управления покинет блок инструкций, следующий ниже. Во втором примере инструкция with автоматически захватит блокировку, ког- да поток управления войдет в блок инструкций, следующий ниже, и осво- бодит ее при выходе. Инструкция with obj позволяет объекту obj управлять происходящим, когда поток управления входит и покидает блок инструкций, следующий ниже. Когда интерпретатор встречает инструкцию with obj, он вызывает метод obj.__enter__(), чтобы известить объект о том, что был выполнен вход в новый контекст. Когда поток управления покидает контекст, вызывается метод obj.__exit__(type,value,traceback). Если при выполнении инструкций Менеджеры контекста и инструкция with 127 в блоке не возникло никаких исключений, во всех трех аргументах мето- ду __exit__() передается значение None. В противном случае в них записы- ваются тип исключения, его значение и трассировочная информация об исключении, вынудившем поток управления покинуть контекст. Метод __exit__() возвращает True или False, чтобы показать, было обработано ис- ключение или нет (если возвращается значение False, возникшее исключе- ние продолжит свое движение вверх за пределами контекста). Инструкция with obj может принимать дополнительный спецификатор as var . В этом случае значение, возвращаемое методом obj.__enter__(), записы- вается в переменную var. Важно отметить, что в переменную var не обяза- тельно будет записано значение obj. Инструкция with способна работать только с объектами, поддерживаю- щими протокол управления контекстом (методы __enter__() и __exit__()). Пользовательские классы также могут определять эти методы, чтобы реа- лизовать собственный способ управления контекстом. Ниже приводится простой пример такого класса: class ListTransaction(object): def __init__(self,thelist): self.thelist = thelist def __enter__(self): self.workingcopy = list(self.thelist) return self.workingcopy def __exit__(self,type,value,tb): if type is None: self.thelist[:] = self.workingcopy return False Этот класс позволяет выполнять серию модификаций в существующем списке. При этом модификации вступят в силу, только если в процессе их выполнения не возникло исключения. В противном случае список останет- ся в первоначальном состоянии. Например: items = [1,2,3] with ListTransaction(items) as working: working.append(4) working.append(5) print(items) # Выведет [1,2,3,4,5] ёё try: with ListTransaction(items) as working: working.append(6) working.append(7) raise RuntimeError(“Немножко смошенничаем!”) except RuntimeError: pass print(items) # Выведет [1,2,3,4,5] Модуль contextlib упрощает реализацию собственных менеджеров контек- ста за счет обертывания функций-генераторов. Например: from contextlib import contextmanager @contextmanager 128 Глава 5. Структура программы и управление потоком выполнения def ListTransaction(thelist): workingcopy = list(thelist) yield workingcopy # Изменить оригинальный список, только если не возникло ошибок thelist[:] = workingcopy В этом примере значение, передаваемое инструкции yield, использует- ся, как возвращаемое значение метода __enter__(). После вызова метода __exit__() выполнение будет продолжено с инструкции, следующей за ин- струкцией yield. Если в контексте возникло исключение, оно проявится как исключение в функции-генераторе. При необходимости функция мо- жет перехватить и обработать исключение, но в данном случае оно продол- жит свое распространение за пределы генератора и, возможно, будет обра- ботано где-то в другом месте. Отладочные проверки и переменная __debug__ Инструкция assert позволяет добавлять в программу отладочный код. В общем случае инструкция assert имеет следующий вид: assert test [, msg] где test – выражение, которое должно возвращать значение True или False. Если выражение test возвратит значение False, инструкция assert возбудит исключение AssertionError с переданным ему сообщением msg. Например: def write_data(file,data): assert file, “write_data: файл не определен!” ... Инструкция assert не должна содержать программный код, обеспечиваю- щий безошибочную работу программы, потому что он не будет выполнять- ся интерпретатором, работающим в оптимизированном режиме (этот ре- жим включается при запуске интерпретатора с ключом -O). В частности, будет ошибкой использовать инструкцию assert для проверки ввода поль- зователя. Обычно инструкции assert используются для проверки условий, которые всегда должны быть истинными; если такое условие нарушается, это можно рассматривать, как ошибку в программе, а не как ошибку поль- зователя. Например, если функция write_data() в примере выше была бы предназна- чена для взаимодействия с конечным пользователем, инструкцию assert следовало бы заменить обычной условной инструкцией if и предусмотреть обработку ошибок. Помимо инструкции assert в Python имеется встроенная переменная __de- de- bug__ , доступная только для чтения, которая получает значение True, когда интерпретатор работает не в оптимизированном режиме (включается при запуске интерпретатора с ключом -O). Программы могут проверять значе- ние этой переменной, чтобы в случае необходимости выполнять дополни- тельную проверку на наличие ошибок. Внутренняя реализация перемен- Отладочные проверки и переменная __debug__ 129 ной __debug__ в интерпретаторе оптимизирована так, что сама инструкция if в действительности не включается в текст программы. Если интерпрета- тор действует в обычном режиме, программный код, составляющий тело инструкции if __debug__, просто включается в текст программы, без са- мой инструкции if. При работе в оптимизированном режиме инструкция if __debug__ и все связанные с ней инструкции полностью удаляются из программы. Использование инструкции assert и переменной __debug__ обеспечива- ет возможность эффективной разработки программ, работающих в двух режимах. Например, для работы в отладочном режиме можно добавить в свой программный код произвольное количество проверок, чтобы убе- диться в его безошибочной работе. При работе в оптимизированном режи- ме все эти проверки будут удалены и уже не будут отрицательно влиять на производительность. |