Использование сложных конструкций
Шутки кончились
Ошибки и исключения
До этого момента сообщения об ошибках лишь упоминались, но если вы пробовали примеры на практике — возможно, вы уже видели некоторые. Существует (как минимум) два различимых вида ошибок: синтаксические ошибки (syntax errors) и исключения (exceptions).
Синтаксические
ошибки
Синтаксические ошибки, также известные как ошибки разбора кода (парсинга, parsing) — вероятно, наиболее привычный вид жалоб компилятора, попадающихся вам при изучении Python:
>>> while True print('Hello world')
File "<stdin>", line
while True print('Hello world')
^
SyntaxError: invalid syntax
Парсер повторно выводит ошибочную строку и отображает небольшую «стрелку», указывающую на самую первую позицию в строке, где была обнаружена ошибка. Причина ошибки (или по крайней мере место обнаружения) находится в символе, предшествующем указанному: в приведённом примере ошибка обнаружена на месте вызова функции print(), поскольку перед ним пропущено двоеточие (':'). Также здесь выводятся имя файла и номер строки, благодаря этому вы знаете в каком месте искать, если ввод был сделан из сценария.
Исключения
Даже если выражение или оператор синтаксически верны, они могут вызвать ошибку при попытке их исполнения. Ошибки, обнаруженные при исполнении, называются исключениями (exceptions). Они не фатальны: позже вы научитесь перехватывать их в программах на Python. Большинство исключений, правда, как правило, не обрабатываются программами и приводят к сообщениям об ошибке, таким как следующие:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line
ZeroDivisionError: int division or
modulo by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line
NameError: name 'spam' is not
defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line
TypeError: coercing to Unicode: need
string or buffer, int found
Последняя строка сообщения об ошибке описывает произошедшее. Исключения представлены различными типами и тип исключения выводится в качестве части сообщения: в примере это типы ZeroDivisionError, NameError и TypeError. Часть строки, описывающая тип исключения — это имя произошедшего встроенного исключения. Такое утверждение верно для всех встроенных исключений, но не обязано быть истинным для исключений, определённых пользователем (однако, само соглашение — довольно полезное). Имена стандартных исключений — это встроенные идентификаторы (не ключевые слова).
Оставшаяся часть строки описывает детали произошедшего на основе типа исключения, которое было его причиной.
Предшествующая часть сообщения об ошибке показывает контекст, где произошло исключение, в форме стека вызовов. В общем случае она содержит стек, состоящий из списка строк исходного кода; тем не менее, в неё не войдут строки, прочитанные из стандартного ввода.
Обработка
исключений
Существует возможность написать код, который будет перехватывать избранные исключения. Посмотрите на представленный пример, в котором пользователю предлагают вводить число до тех пор, пока оно не окажется корректным целым. Тем не менее, пользователь может прервать программу (используя сочетание клавиш Control-C или какое-либо другое, поддерживаемое операционной системой); заметьте — о вызванном пользователем прерывании сигнализирует исключение KeyboardInterrupt.
>>> while True:
... try:
... x = int(input("Введите, пожалуйста, число: "))
... break
... except
ValueError:
... print("Ой! Это некорректное число. Попробуйте ещё раз...")
...
Оператор try работает следующим образом:
· В начале исполняется блок try (операторы между ключевыми словами try и except).
· Если при этом не появляется исключений, блок except не выполняется и оператор try заканчивает работу.
· Если во время выполнения блока try было возбуждено какое-либо исключение, оставшаяся часть блока не выполняется. Затем, если тип этого исключения совпадает с исключением, указанным после ключевого слова except, выполняется блок except, а по его завершению выполнение продолжается сразу после оператора try-except.
· Если порождается исключение, не совпадающее по типу с указанным в блоке except — оно передаётся внешним операторам try; если ни одного обработчика не найдено, исключение считается необработанным (unhandled exception), и выполнение полностью останавливается и выводится сообщение, схожее с показанным выше.
Оператор try может иметь более одного блока except — для описания обработчиков различных исключений. При этом будет выполнен максимум один обработчик. Обработчики ловят только те исключения, которые возникают внутри соответствующего блока try, но не те, которые возникают в других обработчиках этого же самого оператора try-except. Блок except может указывать несколько исключений в виде заключённого в скобки кортежа, например:
... except (RuntimeError, TypeError, NameError):
... pass
В последнем блоке except можно не указывать имени (или имён) исключений. Тогда он будет действовать как обработчик группы исключений. Используйте эту возможность с особой осторожностью, поскольку таким образом он может с лёгкостью перехватить и фактическую ошибку программиста! Также такой обработчик может быть использован для вывода сообщения об ошибке и порождения исключения заново (позволяя при этом обработать исключение коду, вызвавшему обработчик):
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except IOError as err:
print("I/O error: {0}".format(err))
except ValueError:
print("Не могу преобразовать данные в целое.")
except:
print("Неожиданная ошибка:", sys.exc_info()[0])
raise
У оператора try-except есть необязательный блок else, который, если присутствует, должен размещаться после всех блоков except. Его полезно использовать при наличии кода, который должен быть выполнен, если блок try не породил исключений. Например:
for arg in sys.argv[1:]:
try:
f = open(arg,
'r')
except IOError:
print('не могу открыть', arg)
else:
print(arg, 'содержит', len(f.readlines()), 'строк')
f.close()
Использование блока else предпочтительнее, чем добавление дополнительного кода к блоку try, поскольку исключает неожиданный перехват исключения, которое появилось не по причине выполнения кода, защищенного оператором try-except.
При появлении исключения, оно может иметь ассоциированное значение, также известное как аргумент (argument) исключения. Присутствие и тип аргумента зависят от типа самого исключения.
В блоке except можно указать переменную, следующую за именем исключения. Переменная связывается с экземпляром исключения, аргументы которого хранятся в instance.args. Для удобства, экземпляр исключения определяет метод __str__(), так что вывод аргументов может быть произведён явно, без необходимости отсылки к .args. Таким образом, вы также можете создать/взять экземпляр исключения перед его порождением и добавить к нему атрибуты по желанию.
>>> try:
... raise
Exception('spam', 'eggs')
... except
Exception as inst:
... print(type(inst)) # экземпляр исключения
... print(inst.args) # аргументы хранятся в .args
... print(inst) # __str__ позволяет вывести args явно,
... # но может быть переопределён в подклассах исключения
... x, y = inst # распаковка args
... print 'x =', x
... print 'y =', y
...
<class
'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
Если у исключения есть аргументы, они выводится в качестве последней («детальной») части сообщения о необработанном исключении.
Обработчики исключений перехватывают не только исключения, появившиеся
прямо в блоке try, но также и возбужденные внутри функций, которые были в блоке
try вызваны (даже неявно). Например:
>>> def
this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except
ZeroDivisionError as err:
... print('Перехват ошибки времени исполнения:', err)
...
Перехват ошибки времени исполнения: integer division or modulo by zero
Порождение
исключений
Оператор raise позволяет программисту принудительно породить исключение. Например:
>>> raise NameError('ПриветТам')
Traceback (most recent call last):
File "<stdin>", line
NameError: ПриветТам
Единственный аргумент оператора raise определяет исключение, которое нужно возбудить. Им может быть либо экземпляр исключения, либо класс исключения (класс, дочерний к классу Exception).
Если вам нужно определить, было ли возбуждено исключение, не перехватывая его — упрощённая форма оператора raise позволит возбудить исключение заново:
>>> try:
... raise NameError('ПриветТам')
... except
NameError:
... print('Исключение пролетело мимо!')
... raise
...
Исключение пролетело мимо!
Traceback (most recent call last):
File "<stdin>", line
NameError: ПриветТам
Исключения,
определённые пользователем
В программах можно определять свои собственные исключения — посредством
создания нового класса исключения. В общем случае, исключения должны быть
унаследованы от класса Exception: явно или неявно. Например:
>>> class
MyError(Exception):
... def
__init__(self, value):
... self.value = value
... def
__str__(self):
... return
repr(self.value)
...
>>> try:
... raise
MyError(2*2)
... except
MyError as e:
... print('Поймано моё исключение со значением:', e.value)
...
Поймано моё исключение со значением: 4
>>> raise MyError('ой!')
Traceback (most recent call last):
File "<stdin>", line
__main__.MyError: 'ой!'
В этом примере был перегружен конструктор по умолчанию __init__() класса Exception. Новое поведение отличается лишь созданием нового атрибута value и заменяет поведение по умолчанию, при котором создаётся атрибут args.
Классы исключений могут определять любые действия, которые могут делать все другие классы. Однако, обычно их внутренняя структура довольно проста, и предоставляет лишь некоторые атрибуты, позволяющие обработчикам исключений выяснить информацию об ошибке подробно. При создании модуля, который может породить различные ошибки, обычной практикой будет создание базового класса для исключений, определённых в этом модуле, и подклассов для различных ошибочных состояний:
class Error(Exception):
"""Базовый класс для всех исключений в этом модуле."""
pass
class InputError(Error):
"""Исключение порождается при ошибках при вводе.
Атрибуты:
expression -- выражение на вводе, в котором обнаружена ошибка
message -- описание ошибки
"""
def __init__(self, expression, message):
self.expression = expression
self.message = message
class TransitionError(Error):
"""Порождается, когда операция пытается выполнить неразрешённый переход
из одного состояния в другое.
Attributes:
previous -- состояние в начале перехода
next -- новое состояние, попытка принять которое была принята
message -- описание, по какой причине такой переход невозможен
"""
def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message
Большинство исключений имеет имя, заканчивающееся на «Error», подобно стандартным исключениям.
Много стандартных модулей определяют собственные исключения, сообщающие об ошибках, которые могут появится в определяемых ими модулях.
Определение
действий при подчистке
У оператора try есть другой необязательный блок, предназначенный для операций подчистки, которые нужно выполнить независимо от условий:
>>> try:
... raise
KeyboardInterrupt
... finally:
... print('Прощай, мир!')
...
Прощай, мир!
Traceback (most recent call last):
File "<stdin>", line
KeyboardInterrupt
Блок finally исполняется всегда, когда интерпретатор покидает оператор
try, независимо — были исключения или нет. Если в блоке try появилось
исключение, которое не было обработано в блоке except (или появилось в самих
блоках except или else) — оно порождается заново после выполнения блока
finally. Также блок finally исполняется «по пути наружу», если какой-либо
другой блок оператора try был покинут за счёт одного из операторов: break,
continue или return. Более сложный пример:
>>> def
divide(x, y):
... try:
... result = x /
y
... except
ZeroDivisionError:
... print("деление на
ноль!")
... else:
... print("результат: ", result)
... finally:
... print("выполнение блока finally")
...
>>> divide(2, 1)
результат: 2
выполнение блока finally
>>> divide(2, 0)
деление на ноль!
выполнение блока finally
>>> divide("2", "1")
выполнение блока finally
Traceback (most recent call last):
File "<stdin>", line
File "<stdin>", line
TypeError: unsupported operand
type(s) for /: 'str' and 'str'
Как видите, блок finally выполняется при любом событии. Ошибка TypeError порождается при делении двух строк и не перехватывается блоком except, и поэтому порождается заново сразу после выполнения блока finally.
В приложениях реального мира, блок finally применяется для освобождения внешних ресурсов (таких как файлы или сетевые соединения), независимо от того, было ли их использование удачным.
Предопределённые
действия по подчистке
Некоторые объекты определяют стандартные действия при подчистке, применяемые если объект больше не нужен, независимо от того, удачна была операция использования объекта или нет. Посмотрите на следующий пример, в которым мы пытаемся открыть файл и вывести его содержимое на экран.
for line in
open("myfile.txt"):
print(line)
Проблема этого кода в том, что он оставляет файл открытым на неопределённое количество времени после выполнения данной части кода. В простых сценариях это не является проблемой, но может стать ей в больших приложениях. Оператор with позволяет использовать объекты (такие как, например, файлы) таким образом, чтобы вы всегда могли быть уверены в том, что ресурсы будут сразу и корректно очищены.
with open("myfile.txt") as f:
for line in f:
print(line)
После выполнения оператора, файл f всегда закрывается, даже если при прочтении строк обнаружилась проблема. В документации к объектам, которые поддерживают предопределённые действия по подчистке, таким как файлы, эта их способность будет явно указана.
Классы
За счёт механизма классов Python в язык с минимальным использованием нового синтаксиса и семантики добавляется возможность создания классов. Это смесь классовых механизмов, заимствованных из C++ и Modula-3. Как и в случае модулей, классы в Python не устанавливают абсолютного барьера между определением и программистом, рассчитывая больше на аккуратность и вежливость последнего — чтобы он не «врывался в определения». Наиболее важные возможности классов, тем не менее, содержат в себе всю возможную мощь: механизм наследования классов поддерживает несколько предков для класса, производный класс может перегружать любые методы своего предка или предков, а любой его метод может вызвать метод предка с таким же именем. Объекты могут содержать произвольное количество закрытых (private) данных.
В терминологии C++, члены класса (включая данные-члены), обычно, открыты (public) (исключая Приватные переменные, описанные ниже), а все функции-члены — виртуальны. Нет специальных конструкторов и деструкторов. Как в Modula-3, нет краткой ссылки на члены объекта из его методов: функция-метод определяется с явным первым аргументом, описывающем объект, который неявно передаётся при вызове. Как в Smalltalk, классы сами по себе являются объектами, хотя и в более широком смысле: в Python все типы данных — объекты. Таким образом обеспечивается семантика для импортирования и переименования. В отличие от C++ и Modula-3 встроенные типы могут использоваться в качестве предков для расширения возможностей пользователем. Кроме того, как в C++, но не как в Modula-3, большинство встроенных операторов со специальным синтаксисом (арифметические операторы, индексирование и т.д.) могут быть переопределены для экземпляров классов.
Пара
слов о терминологии
Обходя стороной поддерживаемую всем миром терминологию, применимую к разговорам о классах, в нашем случае я буду говорить в терминах C++ и Smalltalk. (Предпочёл бы использовать термины языка Modula-3, поскольку Python ближе к ней по объектно-ориентированной семантике, чем к C++, но предполагаю, что немногие читатели слышали о нём.)
Объекты обладают индивидуальностью, и с одним объектом может быть связано несколько имён (в нескольких областях видимости). Такая практика в других языках известна как совмещение имён (aliasing). На первый взгляд, совмещение малозаметно в Python, и его можно без последствий игнорировать при работе с основными неизменяемыми типами (числами, строками, кортежами). Тем не менее, совмещение имён влияет на семантику программного кода Python, работающего с изменяемыми объектами: списками, словарями и большинством типов, описывающих сущности вне программы (файлы, окна и т.п.). Обычно такая практика считается полезной, поскольку псевдонимы работают подобно указателям и вероятно даже превосходят их возможности. Например, передача объекта - дешевая операция, поскольку по реализации передаётся только указатель. Если функция изменяет переданный в качестве аргумента объект, это будет заметно и в месте вызова. За счёт этого пропадает необходимость в двух различных механизмах передачи аргументов.
Области видимости и пространства имён в Python
Прежде чем заняться классами необходимо получить представление о правилах областей видимости в Python. Определения классов проделывают над пространствами имён некоторые ловкие трюки. Чтобы полностью понимать происходящее, нужно знать о принципах работы областей видимости и пространств имён. Эти знания не помешают любому профессиональному программисту на Python.
Давайте начнём с нескольких определений.
Пространство имён (namespace) — это набор связей имён с объектами[50]. В настоящий момент большинство пространств имён реализованы в виде словарей Python, но не стоит заострять на этом внимание (если только по поводу производительности): возможно, в будущем реализация изменится. Примеры пространств имён: набор встроенных имён (функции вроде abs() и имён встроенных исключений); глобальные имена в модуле; локальные имена при вызове функции. Важная вещь, которую необходимо знать о пространствах имён — это то, что нет абсолютно никакой связи между именами в разных пространствах имён: например, два разных модуля могут без проблем определять функцию «maximize», так как пользователи модулей будут использовать имена модулей в качестве префиксов.
Кстати, слово атрибут (attribute) я применяю к любому имени, следующему за точкой. Например, в выражении z.real, real — это атрибут объекта z. Строго говоря, ссылки на имена в модуле являются ссылками на атрибуты: в выражении имя_модуля.имя_функции под имя_модуля скрывается объект модуля, а под имя_функции — его атрибут. В таком случае обнаруживается прямая связь между атрибутами модуля и глобальными именами, определёнными в модуле: они разделяют между собой одно и тоже пространство имён[51].
Запись в атрибуты может быть запрещена (атрибут только для чтения, read-only attribute) или разрешена (перезаписываемый атрибут, writable attribute). В последнем случае присваивание атрибуту является возможным. Атрибуты модуля перезаписываемы: вы можете написать "modname.the_answer = 42"[52]. Перезаписываемые атрибуты могут также быть удалены оператором del. Например, код "del modname.the_answer" удалит атрибут the_answer из объекта с именем modname.
Пространства имён создаются в различные моменты и имеют разное время жизни. Пространство имён, содержащее встроенные имена создаётся при запуске интерпретатора и не удаляется никогда. Глобальное пространство имён модуля создаётся при вычитке определения модуля. Обычно, пространства имён модулей также «живут» до выхода из интерпретатора. Выражения, выполняемые верхне-уровневым порождением интерпретатора, прочитанные из файла сценария или интерактивно, рассматриваются как часть модуля под названием __main__, поэтому у них есть своё собственное глобальное пространство имён. (Встроенные имена по факту также живут в модуле, он называется builtins).
Локальное пространство имён функции создаётся при её вызове и удаляется когда функция возвращает значение либо порождает исключение, внутри неё не перехваченное. (На самом деле, лучшим способом объяснить, что происходит на самом деле, было бы «забывание»). Конечно же, рекурсивные порождения имеют свои пространства имён каждое.
Область видимости (scope) — это текстовая область в программе на Python, на которой прямым образом доступно пространство имён. «Доступно прямым образом» подразумевает, что явная ссылка на имя вынуждает интерпретатор искать это имя в пространстве имён.
Несмотря на то, что области видимости определяются статически, используются они динамически. В любой момент во время выполнения существует как минимум три вложенных области видимости, чьи пространства имён доступны прямым образом: самая внутренняя[53] область видимости (по ней поиск осуществляется в первую очередь) содержит локальные имена; пространства имён всех объемлющих [данный код] функций, поиск по которым начинается с ближайшей объемлющей [код] области видимости; область видимости среднего уровня, по ней следующей проходит поиск и она содержит глобальные имена текущего модуля; и самая внешняя область видимости (заключительный поиск) — это пространство имён, содержащее встроенные имена.
Если имя определено глобально, тогда все ссылки и присваивания уходят прямо в область видимости среднего уровня, содержащую глобальные имена модуля. Чтобы сменить привязку у всех переменных, найденных вне самой внутренней области видимости, можно использовать оператор nonlocal; если такая переменная не объявлена как nonlocal, то она используется только для чтения (попытка записать значение в такую переменную создаст новую локальную переменную в самой внутренней области видимости, оставляя идентично названную вовне переменную без изменений).
Обычно локальная область видимости ссылается на локальные имена текущей (на уровне текста) функции. Вне функций локальная область видимости ссылается на то же пространство имён, что и глобальная область видимости: пространство имён модуля. Определения классов помещают в локальную область видимости ещё одно пространство имён.
Важно осознавать, что области видимости ограничиваются на текстовом уровне: глобальная область видимости функции, определённая в модуле, является пространством имён этого модуля, независимо от того, откуда или по какому псевдониму была эта функция вызвана. С другой стороны, фактический поиск имён осуществляется динамически, во время выполнения. Как бы то ни было, язык развивается в сторону статического разрешения имён (во время компиляции), так что не стоит полагаться на динамическое разрешение имён. (Фактически, локальные переменные уже определены статично.)
Особая хитрость в Python состоит в том, что — при условии, что в данной области не включены операторы global или nonlocal — присваивания именам всегда уходят в самую внутреннюю область видимости. Присваивания не копируют данных, а лишь связывают имена с объектами. Тоже самое верно и для удалений: оператор "del x" удаляет связь x из пространства имён, на которое ссылается локальная область видимости. В действительности, все операции вводящие новые имена, используют локальную область видимости: в частности, операторы импорта и описаний функций связывают имя модуля или функции в локальной области видимости соответственно. (Для того, чтобы указать определённой переменной, что она должна быть расположена в глобальной области видимости, может использоваться оператор global.)
Оператор global можно использовать для того, чтобы объявить определённые переменные как привязанные к глобальной области видимости и указывает, что их переназначения должны происходить в ней; оператор nonlocal помечает переменные как привязанные к окружающей их области видимости и указывает, что их переназначения должны происходить в ней.
Пример
по областям видимости и пространствам имён
Приведём пример, показывающий, каким образом можно ссылаться на разные области видимости и пространства имён и как global и nonlocal влияют на привязку переменной.
def scope_test():
def do_local():
spam = "локальный спам"
def do_nonlocal():
nonlocal spam
spam = "нелокальный спам"
def do_global():
global spam
spam = "глобальный спам"
spam = "тестовый спам"
do_local()
print("После локального присваивания:", spam)
do_nonlocal()
print("После нелокального присваивания:", spam)
do_global()
print("После глобального присваивания:", spam)
scope_test()
print("В глобальной области видимости:", spam)
Вывод кода из примера таков:
После локального присваивания: тестовый спам
После нелокального присваивания: нелокальный спам
После глобального присваивания: нелокальный спам
В глобальной области видимости: глобальный спам
Заметьте, что локальное присваивание (работающее по умолчанию) не заменяет глобальную привязку на связывание из scope_test. Нелокальное присваивание заменило глобальную привязку на связывание из scope_test, а глобальное присваивание заменило привязку на связывание на уровне модуля.
Можно увидеть, что до глобального присваивания у переменной spam не было предшествующих связываний.
Первый
взгляд на классы
В описании классов представлено немного нового синтаксиса, три новых типа объектов[54] и некоторое количество новой семантики.
Синтаксис
определения класса
Простейшая форма определения класса выглядит так:
class ИмяКласса:
<оператор-1>
.
.
.
<оператор-N>
Определения классов, как и определения функций (операторы def), должны быть исполнены для того, чтобы возыметь действие. (Вы можете, предположим, поместить определение класса в ветку оператора if или внутрь функции.)
На практике, внутри определения класса обычно помещаются определения функций, но позволено использовать и другие операторы — и иногда с пользой — как мы увидим позже. Определения функций внутри класса имеют особенную форму списка аргументов, в связи с соглашениями по вызову методов — опять же, это будет рассмотрено ниже.
При вводе определения класса создаётся новое пространство имён, которое и используется в качестве локальной области видимости. Таким образом, все присваивания локальным переменным происходят в этом новом пространстве имён. В частности, определения функций связываются здесь с именами новых функций.
При успешном окончании парсинга определения класса (по достижении конца определения), создаётся объект-класс (class object). По существу, это обёртка вокруг содержимого пространства имён, созданного во время определения класса; подробнее объекты классов мы изучим в следующем разделе. Оригинальная локальная область видимости (та, которая действовала в последний момент перед вводом определения класса) восстанавливается, а объект-класс тут же связывается в ней с именем класса, указанном в заголовке определения класса (в примере — ИмяКласса).
Объекты-классы
Объекты-классы поддерживают два вида операций: ссылки на атрибуты и создание экземпляра.
Ссылки на атрибуты (Attribute references) используют стандартный синтаксис, использующийся для всех ссылок на атрибуты в Python: объект.имя. Корректными именами атрибутов являются все имена, которые находились в пространстве имён класса при создании объекта-класса. Таким образом, если определение класса выглядело так:
class MyClass:
"""Простой пример класса"""
i = 12345
def f(self):
return 'привет мир'
то MyClass.i и MyClass.f являются корректными ссылками на атрибуты, возвращающими целое и объект-функцию (function object) соответственно. Атрибутам класса можно присваивать значение, так что вы можете изменить значение MyClass.i через присваивание. __doc__ также является корректным атрибутом, возвращающим строку документации, принадлежащей классу: "Простой пример класса".
Создание экземпляра класса использует синтаксис вызова функции. Просто представьте, что объект-класс — это непараметризированная функция, которая возвращает новый экземпляр класса. Например (предполагая класс, приведённый выше):
x = MyClass()
создаёт новый экземпляр класса и присваивает этот объект локальной переменной x.
Операция создания экземпляра (instantiation) создаёт объект данного класса. Большая часть классов предпочитает создавать экземпляры, имеющие определённое начальное состояние. Для этого класс может определять специальный метод под именем __init__(), например так:
def __init__(self):
self.data = []
Когда в классе определён метод __init__(), при создании экземпляра автоматически вызывается __init__() нового, только что созданного объекта. Так, в этом примере, новый инициализированный экземпляр может быть получен за счёт выполнения кода:
x = MyClass()
Конечно же, для
большей гибкости, метод __init__() может иметь параметры. В этом случае
аргументы, переданные оператору создания экземпляра класса, передаются методу
__init__(). Например,
>>> class
Complex:
... def
__init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0,
-4.5)
>>> x.r, x.i
(3.0, -4.5)
Объекты-экземпляры
Теперь, что же мы можем делать с объектами-экземплярами? Единственные операции, доступные объектам-экземплярам — это ссылки на атрибуты. Есть два типа корректных имён атрибутов — это атрибуты-данные и методы.
Атрибуты-данные (data attributes) аналогичны «переменным экземпляров» в Smalltalk и «членам-данным» в C++. Атрибуты-данные не нужно описывать: как и переменные, они начинают существование в момент первого присваивания. Например, если x — экземпляр созданного выше MyClass, следующий отрывок кода выведет значение 16, не вызвав ошибок:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
Другой тип ссылок на атрибуты экземпляра — это метод (method). Метод — это функция, «принадлежащая» объекту. (В Python термин не уникален для экземпляров класса: другие объекты также могут иметь методы. Например, объекты-списки имеют методы append, insert, remove, sort и т.п. Тем не менее, ниже под термином «метод» мы будем понимать только методы объектов-экземпляров классов, пока отдельно не будет указано иное.)
Корректные имена методов объектов-экземпляров зависят от их класса. По определению, все атрибуты класса, являющиеся объектами-функциями, описывают соответствующие методы его экземпляров. Так, в нашем примере, x.f является корректной ссылкой на метод, а x.i ей не является, поскольку не является и MyClass.i. Но при этом x.f — это не то же самое, что MyClass.f: это объект-метод, а не объект-функция.
Объекты-методы
Обычно, метод вызывают сразу после его связывания [с функцией]:
x.f()
На примере MyClass такой код возвратит строку 'привет мир'. Однако, не обязательно вызывать метод так уж сразу: x.f — это объект-метод, он может быть отложен и вызван когда-либо позже. Например:
xf = x.f
while True:
print(xf())
будет печатать 'привет мир' до конца времён.
Что конкретно происходит при вызове метода? Вы, возможно, заметили, что x.f() выше был вызван без аргументов, хотя в описании функции f аргумент был указан. Что же случилось с аргументом? Несомненно, Python порождает исключение когда функция, требующая присутствия аргумента, вызвана без единого — даже, если он на самом деле не используется...
Теперь вы, возможно, догадались: отличительная особенность методов состоит в том, что в качестве первого аргумента функции передаётся объект. В нашем примере вызов x.f() полностью эквивалентен вызову MyClass.f(x). В общем случае, вызов метода со списком из n аргументов эквивалентен вызову соответствующей функции со списком аргументов, созданным за счёт вставки объекта, вызвавшего метод, перед первым аргументом.
Если вы всё ещё не поняли, как работают методы, взгляд на реализацию возможно прояснит происходящее. Когда атрибут экземпляра ссылается на что-либо, не являющееся атрибутом-данными, производится поиск по классу. Если имя указывает корректный атрибут класса, являющийся объектом-функцией, создаётся метод: через упаковку (указателя на) объекта-экземпляра и найденного объекта-функции в абстрактный объект, получается объект-метод. Когда объект-метод вызывается со списком аргументов, он снова распаковывается и новый список аргументов конструируется из объекта-экземпляра и оригинального списка аргументов, и затем уже с новым списком аргументов вызывается объект-функция.
Различные замечания
Атрибуты-данные переопределяют атрибуты-методы с тем же именем; для того, что обезопасить себя от случайных конфликтов имён, которые могут привести к трудно-обнаруживаемым ошибкам в больших программах, разумно использовать какое-нибудь соглашение, которое могло бы уменьшить шансы возникновения конфликтов. Возможные соглашения включают в себя: написание имён методов строчными буквами, предварение имени атрибутов-данных некоторой короткой уникальной строкой (предположим, лишь символом подчёркивания ("_")), или использование глаголов для именования методов и существительных для именования данных.
Методы могут ссылаться на атрибуты-данные также как и обычные пользователи («клиенты») объекта. Другими словами, классы не подходят для разработки чистых абстрактных типов данных. Фактически же в Python нет ничего, вынуждающего вас скрывать данные: сокрытие основано на соглашении между программистами. (С другой стороны, реализация Python, написанная на C, может полностью скрывать детали разработки и, если нужно, контролировать доступ к объекту, это можно делать в расширениях для Python, написанных на C.)
Клиенты должны использовать атрибуты-данные с осторожностью, так как иначе они могут нарушить инварианты, подразумеваемые методами класса при использовании атрибутов-данных. Заметьте, что обычно клиенты могут добавлять собственные атрибуты-данные к объектам-экземплярам, не нарушая работы методов, если не происходит конфликтов имён. Опять же, соглашение об именовании может избавить вас от головной боли и в этих случаях.
У методов нет краткой записи для ссылок изнутри на атрибуты-данные (и другие методы!). Я нахожу, что это и вправду повышает читабельность методов: нет шанса спутать локальные переменные и переменные экземпляров при просмотре тела метода.
Обычно, первый аргумент метода называется self. Это не более чем соглашение: имя self не имеет абсолютно никакого специального смысла для языка Python. (Однако, обратите внимание, что если вы не следуете соглашениям, ваш код может стать менее читабелен для других программистов; и также, потенциально, программа навигации по классам может опираться на такие соглашения.)
Любой объект-функция, являющийся атрибутом класса, определяет метод для экземпляров этого класса. Не так важно, чтобы текст определения функции был заключен в определение класса: присваивание объекта-функции локальной переменной класса также работает неплохо. Например:
# Функция, определённая вне класса
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'привет мир'
h = g
Теперь f, g и h — все являются атрибутами класса C, ссылающимися на объекты-функции, и следовательно, все они являются методами экземпляров C — h становится полностью эквивалентен g. Заметьте, что такая практика обычно лишь запутывает читателя программы.
Методы могут вызывать другие методы за счёт использования атрибутов-методов аргумента self:
class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
Методы могут ссылаться на глобальные имена таким же образом, как и обычные функции. Глобальная область видимости, связанная с методом — это модуль, содержащий определение класса. (Сам класс никогда не используется в качестве глобальной области видимости!) В то время, как одни редко находят причины для использования глобальных данных в методах, существует множество разумных причин использовать глобальную область видимости: для примера, функции и модули, импортированные в глобальную область видимости, могут использоваться в методах так же, как в функциях и классах, в ней определённых. Обычно класс, содержащий метод, сам определён в этой глобальной области видимости, и в следующем разделе мы найдём пару хороших причин, почему методу может быть необходимо ссылаться на собственный класс!
Наследование
Конечно же, не поддерживай «класс» наследование, не стоило бы называть его «классом». Синтаксис производного класса выглядит так:
class ИмяПроизводногоКласса(ИмяБазовогоКласса):
<оператор-1>
.
.
.
<оператор-N>
Имя ИмяБазовогоКласса должно быть определено в области видимости, содержащей определение производного класса. Вместо имени базового класса также позволяется использовать другие выражения. Это может быть полезно, например, когда базовый класс определён в другом модуле:
class ИмяПроизводногоКласса(имямодуля.ИмяБазовогоКласса):
Использование определения производного класса проходит таким же образом, как и базового. Базовый класс полностью сохраняется по завершению конструирования объекта-класса. Такой метод используется для разрешения ссылок на атрибуты[55]: если запрошенный атрибут не был найден в самом классе, поиск продолжается в базовом классе. Правило применяется рекурсивно, если базовый класс сам является производным от некоторого другого класса.
В создании экземпляров производных классов нет ничего особенного: ИмяПроизводногоКласса() создаёт новый экземпляр класса. Ссылки на методы разрешаются следующим образом: производится поиск соответствующего атрибута класса (спускаясь вниз по цепочке базовых классов, если необходимо) и ссылка на метод считается корректной, если она порождает объект-функцию.
Производные классы могут перегружать методы своих базовых классов. Поскольку у методов нет особых привилегий при вызове других методов того же объекта, метод базового класса, вызывающий другой метод, определённый в этом же классе, может вызвать перегруженный метод производного класса. (Для программистов на C++: все методы в Python фактически виртуальны.)
При перегрузке метода в производном классе возможна не только замена действия метода базового класса с тем же именем, но и его расширение. Существует простой способ вызвать метод базового класса прямым образом: просто вызовите "ИмяБазовогоКласса.имяметода(self, аргументы)". Такой способ будет неожиданно полезным и для клиентов. (Обратите внимание, что он работает только если базовый класс определён и импортирован прямо в глобальную область видимости.)
В языке Python есть функции, которые работают с наследованием:
Используйте isinstance() чтобы проверить тип объекта: isinstance(obj, int) возвратит True только если obj.__class__ является int или некоторым классом, наследованным от int.
Используйте issubclass() чтобы проверить наследственность класса: issubclass(bool, int) возвратит True, поскольку класс bool является наследником (subclass) int. Однако, issubclass(float, int) возвратит False, поскольку класс float не является наследником int.
Множественное
наследование
Python также поддерживает форму множественного наследования (multiple inheritance). Определение класса с несколькими базовыми классами будет выглядеть так:
class ИмяПроизводногоКласса(Базовый1, Базовый2, Базовый3):
<оператор-1>
.
.
.
<оператор-N>
В простейших случаях и для большинства задач, вы можете представлять себе поиск атрибутов, наследованных от родительского класса в виде «сперва вглубь», затем «слева-направо». Таким образом, если атрибут не найден в ИмяПроизводногоКласса, его поиск выполняется в Базовом1, затем (рекурсивно) в базовых классах Базового1 и только если он там не найден, поиск перейдёт в Базовый2 и так далее.
На самом деле всё немного сложнее. Порядок разрешения методов[56] (method resolution order) меняется динамически, чтобы обеспечить возможность сотрудничающих вызовов super(). Этот способ известен в некоторых других языках с поддержкой множественного наследования как "вызов-следующего-метода" („call-next-method“) и имеет больше возможностей, чем вызов родительского метода в языках с единичным наследованием.
Динамическое упорядочивание (dynamic ordering) имеет важность, поскольку все вариации множественного наследования проявляют в себе эффект ромбовых отношений (когда как минимум один родительский класс может быть доступен различными путями из низшего в иерархии класса). Например, все классы наследуются от object, так что множественное наследование в любом виде предоставляет более одного пути для того, чтобы достичь object. Чтобы защитить базовые классы от двойных и более запросов, динамический алгоритм «выпрямляет» (linearizes) порядок поиска таким образом, что тот сохраняет указанный слева-направо порядок для каждого класса, который вызывает каждый родительский класс только единожды и является монотонным (значит, класс можно сделать наследником, не взаимодействуя с порядком предшествования его родителей). Обобщённые вместе, эти свойства позволяют разрабатывать надёжные и расширяемые классы, используя множественное наследование. С подробностями можно ознакомиться по этой ссылке: http://www.python.org/download/releases/2.3/mro/ (перевод).
Приватные
переменные
В Python имеется ограниченная поддержка идентификаторов, приватных для класса. Любой идентификатор в форме __spam (как минимум два предшествующих символа подчёркивания, как максимум один завершающий) заменяется дословно на _classname__spam, где classname — текущее имя класса, лишённое предшествующих символов подчёркивания. Это искажение (mangling) производится без оглядки на синтаксическую позицию идентификатора, поэтому может использоваться для определения переменных, приватных для класса экземпляров, переменных класса, методов, переменных в глобальной области видимости (globals), и даже переменных, использующихся в экземплярах, приватных для этого класса на основе экземпляров других классов. Имя может быть обрезано, если его длина превышает 255 символов. Вне классов, или когда имя состоит из одних символов подчёркивания, искажения не происходит.
Предназначение искажения имён состоит в том, чтобы дать классам лёгкую возможность определить «приватные» переменные экземпляров и методы, не беспокоясь о переменных экземпляров, определённых в производных классах, и о забивании кодом вне класса переменных экземпляров. Обратите внимание, что правила искажения имён разработаны, в основном, чтобы исключить неприятные случайности — решительная душа всё ещё может получить доступ или изменить переменные, предполагавшиеся приватными. В некотором особом окружении, таком как отладчик, это может оказаться полезным — и это единственная причина, по которой лазейка не закрыта. (Внимание: наследование класса с таким же именем как и у базового делает возможным использование приватных переменных базового класса.)
Заметьте, что код, переданный в exec() или eval(), не предполагает в качестве текущего имени класса имя класса, порождающего вызов — так же, как и в случае эффекта с оператором global — эффекта, который также ограничен для всего побайтно-компилирующегося кода. И, такое же ограничение применимо для функций getattr(), setattr() и delattr(), и также для прямой ссылки на __dict__.
Всякая
всячина
Иногда бывает полезен тип данных, похожий на record из языка Pascal или struct из языка C, например, для хранения нескольких поименованных элементов данных. Для этой цели подойдет даже пустое определение класса[57]:
class Employee:
pass
john = Employee() # Создать пустую запись о рабочем
# Заполнить поля записи
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
Фрагменту кода на Python, требующему на входе некоторого абстрактного типа данных, можно дать экземпляр, эмулирующий методы этого типа данных. Например, если имеется функция, умеющая форматировать данные из файлового объекта, то можно определить класс с методами read() и readline() (работающие с данными, скажем, из строкового буфера) и передать ей экземпляр этого класса в качестве аргумента.
Объекты-методы экземпляров также имеют атрибуты: m.__self__ — исходный объект-экземпляр с методом m(), а m.__func__ — объект-функция, соответствующий методу.
Исключения
— тоже классы
Исключения, определённые пользователем, могут быть также отождествлены с классами. При использовании этого механизма становится возможным создавать расширяемые иерархии исключений.
Оператор raise имеет следующие (синтаксически) правильные формы:
raise Класс
raise Экземпляр
В первой форме, Класс должен быть экземпляром типа или класса, производного от него. Первая форма является краткой записью следующего кода:
raise Class()
Класс в блоке except является сопоставимым с исключением, если является этим же классом или самим по себе базовым классом (никаких других способов обхода — описанный в блоке except производный класс не сопоставим с базовым). Например, следующий код выведет B, C, D в этом порядке:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for c in [B, C, D]:
try:
raise c()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Обратите внимание, что если бы блоки except шли в обратном порядке (начиная с "except B"), код вывел бы B, B, B — сработал бы первый совпадающий блок except.
При выводе сообщения об ошибке о необработанном исключении, выводится класс исключения, затем двоеточие и пробел, и наконец экземпляр, приведённый к строке за счёт встроенной функции str().
Итераторы
К этому моменту вы, возможно, заметили, что используя оператор for можно организовать цикл по большинству объектов-контейнеров:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'один':1, 'два':2}:
print(key)
for char in "123":
print(char)
for line in
open("myfile.txt"):
print(line)
Такой стиль доступа к элементам прост, лаконичен и удобен. Использованием итераторов (iterators) пропитан язык Python, и это его выделяет среди других. Негласно, оператор for вызывает метод iter() объекта-контейнера. Функция возвращает объект итератора, который определяет метод __next__(), который по очереди получает доступ к элементам в контейнере, по одному за раз. Если больше не остаётся элементов, метод __next__() порождает исключение StopIteration, которое сообщает оператору for о необходимости завершения прохода. Вы можете вызывать метод __next__() посредством встроенной функции next(); следующий пример показывает, как это работает:
>>> s = 'абв'
>>> it
= iter(s)
>>> it
<iterator
object at 0x00A1DB50>
>>> next(it)
'а'
>>> next(it)
'б'
>>> next(it)
'в'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line
next(it)
StopIteration
Ознакомившись с механизмами, скрытыми за протоколом итераторов, легко добавить возможность итерирования к вашим классам. Определите метод __iter__(), который возвращает объект с методом next(). Если класс определяет и метод next(), тогда __iter__() может просто возвращать self.
class Reverse:
"Итератор по последовательности в обратном направлении"
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index
== 0:
raise
StopIteration
self.index = self.index - 1
return
self.data[self.index]
>>> for
char in Reverse('спам'):
... print(char)
...
м
а
п
с
Генераторы
Генераторы (generators) — простой и мощный инструмент для создания итераторов. Они записываются как обычная функция, но где бы им ни было необходимо вернуть данные, используется оператор yield. Каждый раз, когда над ним вызывается next(), генератор возвращается к месту, где он был оставлен (он запоминает все значения данных, а также какой оператор был выполнен последним). Пример показывает, что создание генераторов может быть тривиально простым:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield
data[index]
>>> for
char in reverse('гольф'):
... print(char)
...
ф
ь
л
о
г
Всё, что можно сделать с использованием генераторов, может быть сделано с использованием основанных на итераторах классов, как описано в предыдущем разделе. Благодаря автоматическому созданию методов __iter__() и __next__() генераторы так компактны.
Другая важная особенность состоит в том, что между вызовами сохраняются локальные переменные и состояние выполнения (execution state). Это позволяет конструкциям функций быть проще, а получению переменных экземпляров быть намного легче, нежели с использованием self.index и self.data.
В дополнение к автоматическому созданию методов и сохранению состояния, когда генераторы заканчивают своё действие, они автоматически порождают исключение StopIteration. В комбинации, эти особенности позволяют легко создавать итераторы не прилагая усилий больших, чем нужно для написания обычной функции.
Выражения-генераторы
Некоторые простые генераторы могут быть сжато закодированы в выражении с использованием синтаксиса, схожего со списковыми сборками, но с круглыми скобками заместо квадратных. Выражения-генераторы разработаны в основном для случаев, когда генератор тут же используется в качестве аргумента функции. Выражения с генераторами более компактные, но менее гибкие чем полные определения генераторов и обычно используют память экономнее, чем эквивалентные списковые сборки.
Примеры:
>>> sum(i*i
for i in range(10)) # сумма квадратов
285
>>> xvec
= [10, 20, 30]
>>> yvec
= [7, 5, 3]
>>> sum(x*y
for x,y in zip(xvec, yvec)) # скалярное произведение
260
>>> from
math import sin, radians
>>> sine_table = {x: sin(radians(x)) for x in range(0, 91)}
>>> unique_words = set(word for line in
page for word in line.split())
>>> valedictorian
= max((student.gpa, student.name) for student in graduates)
>>> data
= 'golf'
>>> list(data[i]
for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']
Краткий
обзор стандартной библиотеки
Взаимодействие
с операционной системой
Модуль os предоставляет десятки функций для взаимодействия с операционной системой.
>>> import os
>>> os.getcwd() # возвращает путь к текущему каталогу
'C:\\Python31'
>>> os.system('dir *.txt') # выполнить указанную команду ОС
...список текстовых файлов, выведенных командой...
0
>>> os.chdir('/server/accesslogs') # сменить текущий каталог
Лучше всего применять import os вместо from os import *. Это предохранит встроенную функцию open() от замещения функцией os.open(), имеющей несколько иное назначение.
В интерактивном режиме встроенные функции dir() и help() помогут разобраться с большими модулями вроде os:
>>> import os
>>> dir(os)
...список всех атрибутов модуля...
>>> help(os)
...страница руководства по модулю на основе строк документации...
Для управления файлами и каталогами удобный высокоуровневый интерфейс предоставляет модуль shutil:
>>> import shutil
>>> shutil.copyfile('data.db', 'archive.db') # копировать файл
>>> shutil.move('/build/executables',
'installdir') # переместить каталог
Wildcard-шаблоны
для имён файлов
Модуль glob предоставляет функцию для получения списка файлов на основе заданного шаблона:
>>> import glob
>>> glob.glob('*.py')
['primes.py', 'random.py',
'quote.py']
Аргументы
командной строки
Сценарии общего назначения часто нуждаются в аргументах командной строки. Эти аргументы хранятся в атрибуте argv модуля sys в виде списка. Например, при запуске python demo.py one two three в командной строке шелла для сценария demo.py:
import sys
print(sys.argv)
будет выведено ['demo.py', 'one', 'two', 'three'].
Модуль getopt обрабатывает sys.argv, используя соглашения функции getopt() системы Unix. Более мощную и гибкую обработку аргументов командной строки осуществляет модуль optparse.
Стандартный
вывод. Завершение сценария
Модуль sys имеет атрибуты stdin, stdout и stderr (для стандартного ввода, вывода и вывода ошибок соответственно). Последний может быть полезен для вывода предупреждений и сообщений об ошибках когда стандартный вывод перенаправлен
>>> sys.stderr.write('Внимание! Файл журнала не найден.\n')
Внимание! Файл журнала не найден.
34
Для завершения сценария можно использовать sys.exit().
Сравнение
строк по шаблонам
Модуль re предоставляет инструментарий для работы с регулярными выражениями для нетривиальной обработки текста. С помощью регулярных выражений можно создавать выразительные и оптимизированные решения для поиска и обработки строк.
>>> import re
>>> re.findall(r'\bf[a-z]*',
'which foot or hand fell fastest')
['foot', 'fell', 'fastest']
>>> re.sub(r'(\b[a-z]+) \1', r'\1', 'cat in the the hat')
'cat in the hat'
Когда требуются более простые возможности, методы строковых объектов предпочтительнее, так как их легче читать и отлаживать:
>>> 'чай для двоих'.replace('для', 'на')
'чай на двоих'
Математические
функции
Модуль math предоставляет доступ к библиотеке языка C для функций над числами с плавающей запятой:
>>> import math
>>> math.cos(math.pi
/ 4)
0.70710678118654757
>>> math.log(1024, 2)
10.0
С помощью модуля random можно делать случайный выбор:
>>> import
random
>>> random.choice(['яблоко', 'груша', 'банан'])
'яблоко'
>>> random.sample(range(100), 10) # выборка без повторений
[30, 83, 16, 4, 8, 81, 41, 50, 18, 33]
>>> random.random() # случайное число с плавающей запятой
0.17970987693706186
>>> random.randrange(6) # случайное целое из диапазона range(6)
4
>>> random.sample([1, 2, 3, 4, 5], 3) # случайные три элемента из списка
[4, 1, 5]
Проект SciPy <http://scipy.org> имеет много других модулей для численных расчётов.
Протоколы
интернет
В стандартной библиотеке имеется целый набор модулей для различных сервисов и протоколов интернет. Наиболее употребимыми можно считать urllib.request для получения данных по заданному адресу (URL) и smtplib для отправки сообщений электронной почты:
>>> from
urllib.request import urlopen
>>> for
line in urlopen('http://tycho.usno.navy.mil/cgi-bin/timer.pl'):
... if 'EST' in line
or 'EDT' in line: # временные зоны
... print(line)
<BR>Nov. 25, 09:43:32 PM EST
>>> import smtplib
>>> server
= smtplib.SMTP('localhost')
>>> server.sendmail('soothsayer@example.org',
'jcaesar@example.org',
... """To:
jcaesar@example.org
... From: soothsayer@example.org
...
... Beware the Ides of March.
... """)
>>> server.quit()
Заметьте, что второй пример требует почтового сервера на той же машине.
Дата
и время
Модуль datetime предлагает классы для работы с датами и временем как в простых, так и сложных случаях. Кроме поддержки календарной арифметики, реализация обращает особенное внимание на эффективность вычисления составных частей для вывода и дальнейших манипуляций. Модуль также предлагает объекты с поддержкой временных зон.
# даты можно легко составлять и выводить в требуемом формате
>>> from
datetime import date
>>> сейчас =
date.today()
>>> сейчас
datetime.date(2009, 2, 3)
>>> сейчас.strftime("%d.%m.%Y")
'03.02.2009'
# даты поддерживают календарную арифметику
>>> день_рождения = date(1964, 7, 31)
>>> возраст = сейчас - день_рождения
>>> возраст.days
16258
Сжатие
данных и архивы
Наиболее распространённые форматы сжатия и архивации напрямую поддерживаются модулями стандартной библиотеки: zlib, gzip, bz2, zipfile и tarfile.
>>> import zlib
>>> s = "Закрой замок на замок, чтобы замок не замок"
>>> len(bytes(s,
"utf-8"))
78
>>> t = zlib.compress(s)
>>> len(t)
54
>>> print(str(zlib.decompress(t),
"utf-8"))
Закрой замок на замок, чтобы замок не замок
>>> zlib.crc32(s)
2392363341
Измерение
производительности
Некоторым пользователям Python интересно знать относительную производительность различных подходов к решению одной и той же проблемы. Python предлагает инструмент для немедленного ответа на эти вопросы.
Например, очень заманчиво использовать присваивание кортежей вместо традиционного подхода к замене значений переменных местами. Модуль timeit быстро продемонстрирует незначительное превосходство кортежей:
>>> from timeit import Timer
>>> Timer('a,
b = b, a', 'a=1; b=2').timeit()
0.15707302093505859
>>> Timer('t=a;
a=b; b=t', 'a=1; b=2').timeit()
0.19421601295471191
В отличие от высокого разрешения модуля timeit, модули profile и pstats предлагают возможность обнаружить критические по времени участки в больших фрагментах кода.
Контроль
качества
Один из подходов к разработке высококачественного программного обеспечения заключается в написании тестов для каждой функции при её разработке и регулярном запуске этих тестов во время всего процесса разработки.
Модуль doctest имеет средства для просмотра модуля и проверки результатов тестов, заложенных в строках документации. Написание теста для функции заключается в копировании и вставке типичного вызова функции и ее результатов в строку документации. Это улучшает документацию, предоставляя пользователю реальный пример, а модуль doctest проверяет, что код соответствует документации.
def average(values):
"""Вычисляет среднее арифметическое списка чисел.
>>> print(average([20, 30, 70]))
40.0
"""
return sum(values) / len(values)
import doctest
doctest.testmod() # автоматически проверяет тесты в документации
Модуль unittest не менее прост в использовании чем doctest, но позволяет создавать более полный набор тестов, располагающийся в отдельном файле:
import unittest
class TestStatisticalFunctions(unittest.TestCase):
def test_average(self):
self.assertEqual(average([20,
30, 70]), 40.0)
self.assertEqual(round(average([1,
5, 7]), 1), 4.3)
self.assertRaises(ZeroDivisionError,
average, [])
self.assertRaises(TypeError,
average, 20, 30, 70)
unittest.main() # Вызов из
командной строки выполняет проверку всех тестов этого модуля
«Батарейки
в комплекте»
Python руководствуется философией «батарейки в комплекте». Это можно проследить по сложным и устойчивым к ошибкам возможностям его пакетов побольше. Примеры:
Второй
краткий обзор стандартной библиотеки
Форматирование
вывода
Модуль reprlib предоставляет версию функции repr(), настроенную на сокращённый вывод больших и многократно вложенных контейнеров:
>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"set(['a',
'c', 'd', 'e', 'f', 'g', ...])"
Модуль pprint предлагает более утончённый контроль над выводом встроенных и определённых пользователем объектов способом, подходящим для интерпретатора. Когда результат не умещается на строке, умный pprint добавляет по необходимости разбивку на строки и отступы, помогающие выделить структуру данных:
>>> import pprint
>>> t = [[[['чёрный', 'бирюзовый'], 'белый', ['зелёный', 'красный']], [['пурпурный', 'жёлтый'], 'голубой']]]
>>> pprint.pprint(t, width=30)
[[[['чёрный', 'бирюзовый'],
'белый',
['зелёный', 'красный']],
[['пурпурный', 'жёлтый'],
'голубой']]]
Модуль textwrap форматирует абзацы текста под определённую ширину:
>>> import textwrap
>>> doc = """Метод wrap() аналогичен fill(), но он возвращает список строк, а не одну большую строку с признаками концов строк."""
>>> print(textwrap.fill(doc,
width=40))
Метод wrap() аналогичен fill(), но он
возвращает список строк, а не одну
большую строку с признаками концов
строк.
Модуль locale дает доступ к базе данных форматов различных культурных сред. Например, параметр grouping функции format этого модуля позволяет использовать группировку цифр принятыми в данной культурной среде разделителями:
>>> import locale
>>> locale.setlocale(locale.LC_ALL,
'en_US.UTF8')
'en_US.UTF8'
>>> conv = locale.localeconv() # получить отображение соглашений
>>> x = 1234567.8
>>> locale.format("%d",
x, grouping=True)
'1,234,567'
>>> locale.format("%s%.*f",
(conv['currency_symbol'], conv['frac_digits'], x), grouping=True)
'$1,234,567.80'
Работа
с шаблонами
Модуль string включает в себя гибкий класс Template, реализующий шаблоны с простым синтаксисом, доступным для редактирования конечными пользователями. Использование этого класса позволит пользователям настраивать приложения без изменений в них самих.
Формат использует имена полей для подстановки, записываемых как знак доллара ($) с последующим идентификатором, состоящим, как и имена в программах на Python, из букв, цифр и подчёркиваний[60]. Фигурные скобки вокруг идентификатора позволяют использовать алфавитно-цифровые символы сразу после поля подстановки, без дополнительных пробелов. Собственно знак доллара необходимо записывать сдвоенно: $$.
>>> from string import Template
>>> t = Template('${village}folk send $$10 to $cause.')
>>> t.substitute(village='
'Nottinghamfolk send $10 to the
ditch fund.'
Метод substitute() возбуждает KeyError в случае, когда значение для поля отсутствует в переданных параметрах. Для приложений вроде массовой персонализированной рассылки, часть данных может отсутствовать. В таком случае лучше использовать метод safe_substitute(): он оставит разметку полей подстановки в случае отсутствия данных.[61]
>>> t = Template('Return
the $item to $owner.')
>>> d = dict(item='unladen
swallow')
>>> t.substitute(d)
Traceback (most recent call last):
. . .
KeyError: 'owner'
>>> t.safe_substitute(d)
'Return the unladen swallow to
$owner.'
Производные от Template классы могут переопределить разделитель. Например, утилита переименования файлов просмотрщика фотографий может использовать знаки процента в разметке полей подстановки: текущая дата, номер изображения по порядку, формат файла:
>>> import time, os.path
>>> photofiles
= ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class
BatchRename(Template):
... delimiter = '%'
>>> fmt = input('Введите стиль переименования (%d - дата, %n - номер п/п, %f - формат): ')
Введите стиль переименования (%d - дата, %n - номер п/п, %f - формат): Ashley_%n%f
>>> t = BatchRename(fmt)
>>> date
= time.strftime('%d%b%y')
>>> for
i, filename in enumerate(photofiles):
... base, ext = os.path.splitext(filename)
... newname =
t.substitute(d=date, n=i, f=ext)
... print('{0} -->
{1}'.format(filename, newname))
img_1074.jpg --> Ashley_0.jpg
img_1076.jpg --> Ashley_1.jpg
img_1077.jpg --> Ashley_2.jpg
Другое приложение для использования шаблонов - отделение логики от деталей реализации различных выходных форматов. Это даёт возможность строить шаблоны для XML-файлов, текстовых отчётов и веб-отчётов на HTML.
Работа
с записями двоичных данных
Модуль struct предлагает функции pack() и unpack() для работы с форматами двоичных записей переменной длины. Следующий пример показывает как можно получить заголовочную информацию из ZIP-файла без использования модуля zipfile. Коды "H" и "I" представляют двух- и четырехбайтовых беззнаковых числа соответственно. Код "<" обозначает, что числа стандартного размера и байты записаны в порядке «сначала младший» (little-endian):
import struct
data = open('myfile.zip', 'rb').read()
start = 0
for i in range(3): # показать первые три заголовка
start += 14
fields = struct.unpack('<IIIHH',
data[start:start+16])
crc32, comp_size, uncomp_size, filenamesize, extra_size = fields
start += 16
filename = data[start:start+filenamesize]
start += filenamesize
extra = data[start:start+extra_size]
print(filename, hex(crc32), comp_size,
uncomp_size)
start += extra_size + comp_size # пропустить до следующего заголовка
Многопоточность
Потоки (threads) могут использоваться для разделение задач, которые могут выполняться по времени независимо от друг от друга. Разделение на потоки может применяться для улучшения времени отклика приложений, ведущих диалог с пользователем пока другие задачи выполняются в фоновом режиме. Похожая ситуация с произведением ввода-вывода одновременно с вычислениями в другом потоке.
Следующий пример показывает, как высокоуровневый модуль threading может выполнять фоновые задачи, продолжая выполнение основной программы:
import threading, zipfile
class AsyncZip(threading.Thread):
def __init__(self, infile, outfile):
threading.Thread.__init__(self)
self.infile = infile
self.outfile = outfile
def run(self):
f = zipfile.ZipFile(self.outfile,
'w', zipfile.ZIP_DEFLATED)
f.write(self.infile)
f.close()
print('Завершён фоновый zip для:', self.infile)
background = AsyncZip('mydata.txt',
'myarchive.zip')
background.start()
print('Главная программа на переднем плане.')
background.join() # Ждём завершения фоновой задачи
print('Главная программа дождалась завершения фоновой задачи.')
Основной трудностью многопоточных приложений является координирование потоков, разделяющих общие данные или другие ресурсы. В помощь этому модуль threading предлагает целый ряд простых средств: замки́, события, переменные условий, семафоры и другие.
Несмотря на достаточную мощь представленных средств даже небольшие погрешности в дизайне многопоточного приложения могут вызвать трудноповторимые проблемы. Таким образом, рекомендуемым подходом к координированию задач является централизация доступа к некоторому ресурсу в одном потоке и использование модуля queue для направления запросов из других потоков. Приложения, использующие Queue-объекты для межпотоковой связи и координирова
Запись
в журнал
Модуль logging предлагает богатую возможностями и гибкую систему ведения журнала. В простейшем случае сообщения отправляются на стандартный вывод ошибок - sys.stderr:
import logging
logging.debug('Отладочная информация')
logging.info('Для информации')
logging.warning('Предупреждение: файл %s не найден', 'server.conf')
logging.error('Произошла ошибка!')
logging.critical('Критическая ошибка - выход')
Результат выполнения этого примера:
WARNING:root:Предупреждение: файл server.conf не найден
ERROR:root:Произошла ошибка!
CRITICAL:root:Критическая ошибка - выход
Без дополнительной настройки информационные и отладочные сообщения подавляются, а вывод направляется в sys.stderr. Другие варианты вывода: отправка сообщений по электронной почте, дейтаграммами, сокеты, на HTTP сервер. Другие фильтры могут выбирать различные варианты доставки в зависимости от приоритета: DEBUG, INFO, WARNING, ERROR или CRITICAL.
Система записи в журнал может быть сконфигурирована напрямую из Python. Конфигурация также может быть загружена из конфигурационного файла и не требовать изменений в коде приложения.
Слабые
ссылки
Python имеет автоматическое управление памятью: подсчёт ссылок для большинства объектов и сборка мусора для удаления циклов. Память освобождается сразу после того, как была удалена последняя ссылка на объект.
Этот подход отлично работает для большинства приложений, но иногда возникает необходимость вести учёт объектов только когда они используются где-нибудь ещё. К сожалению, само слежение за объектами уже создает ссылку и тем самым объекты остаются в памяти. Модуль weakref (от англ. weak reference - слабая ссылка) даёт средство для учёта объектов без создания ссылок на них. Когда объект больше не нужен, он автоматически удаляется из таблицы слабых ссылок и производится обратный вызов weakref-объектов. Типичное применение модуля - кэширование объектов, которые затратно воспроизвести снова.
>>> import weakref, gc
>>> class
A:
... def __init__(self,
value):
... self.value = value
... def
__repr__(self):
... return
str(self.value)
...
>>> a = A(10) # создаёт ссылку
>>> d = weakref.WeakValueDictionary() # словарь, использующий слабые ссылки
>>> d['primary'] = a # не создаёт ссылки
>>> d['primary'] # достать объект, если он все ещё "жив"
10
>>> del a # удалить одну ссылку
>>> gc.collect() # произвести сборку мусора
0
>>> d['primary'] # запись была автоматически удалена
Traceback (most recent call last):
File "<stdin>", line
d['primary']
File "C:/python31/lib/weakref.py", line
o = self.data[key]()
KeyError: 'primary'
Работа
со списками
В Python список может заменить многие структуры данных. Однако иногда необходимы альтернативные реализации, которые имеют другое компромиссное решение в отношении производительности.
Модуль array (массив) предоставляет объект array, отличающийся от списка лишь возможностью более компактно хранить однородные данные. Следующий пример показывает массив чисел, хранимый в виде двухбайтных беззнаковых чисел (типокод "H"), а не в обычном списке, где каждый элемент типа int обычно занимает 16 байт:
>>> from
array import array
>>> a = array('H',
[4000, 10, 700, 22222])
>>> sum(a)
26932
>>> a[1:3]
array('H', [10, 700])
Модуль collections (коллекции) среди прочего предоставляет объект deque (дек, двухсторонняя очередь). Объекты этого типа имеют более быстрые операции вставки (append) и извлечения (pop) слева, но более медленный поиск внутренних элементов. Эти объекты хорошо подходят для реализации очередей и деревьев поиска в ширину:
>>> from
collections import deque
>>> d = deque(["задача1", "задача2", "задача3"])
>>> d.append("task4")
>>> print("Обрабатывается", d.popleft())
Обрабатывается задача1
unsearched = deque([starting_node])
def breadth_first_search(unsearched):
node = unsearched.popleft()
for m in gen_moves(node):
if is_goal(m):
return m
unsearched.append(m)
В дополнение к альтернативным реализациям списков библиотека также предлагает средства вроде модуля bisect с функциями для манипуляции отсортированными списками:
>>> import
bisect
>>> scores
= [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
>>> bisect.insort(scores, (300, 'ruby')) # вставка значения в отсортированный список
>>> scores
[(100, 'perl'), (200, 'tcl'), (300,
'ruby'), (400, 'lua'), (500, 'python')]
Модуль heapq имеет функции для реализации кучи, основанной на обычных списках. Элемент с наименьшим значением всегда находится в позиции с индексом 0. Это свойство может с успехом быть задействовано в приложениях, где особенно частый доступ необходим к наименьшему элементу, но полную сортировку проводить накладно.
>>> from
heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data) # сделать из списка кучу
>>> heappush(data, -5) # добавить новый элемент
>>> [heappop(data) for i in range(3)] # извлечь три наименьших значения
[-5, 0, 1]
Десятичная
арифметика чисел с плавающей запятой
Модуль decimal предоставляет тип данных Decimal для десятичной арифметики с плавающей запятой. В сравнении со встроенной двоичной арифметикой, новый класс особенно полезен в финансовых приложениях и других случаях, требующих точного десятичного представления, управления точностью, округлением c соблюдением законодательных и нормативных требований, отслеживания количества значащих цифр или для приложений, от которых ожидается совпадение с результатами, проделанными "вручную".
Например, вычисление 5%-ного налога на 70 копеечный телефонный счет даёт различные результаты при использовании десятичной и двоичной арифметик. Разница становится значащей при округлении до ближайшей копейки:
>>> from decimal import *
>>> Decimal('0.70')
* Decimal('1.05')
Decimal("0.7350")
>>> .70 * 1.05
0.73499999999999999