Модули - структурирование пространства имен
При создании больших программ или библиотек большим количеством людей встает
проблема коллизий имен. Питон решает эту проблему так же, как и большинство
современных языков - структурированием пространства имен с помощью иерархически
организованных модулей.
В Питоне 3 пространства имен: встроенное пространство имен (им можно
управлять с помощью модуля доступа к интерпретатору sys), локальное пространство
функции, и глобальное пространство модуля. (Объектно-ориентированное
программирование создает дополнительные пространства классов и экземпляров
классов, об этом ниже). Каждое пространство имен - это список отображений имени
в значение.
Модуль - это совокупность описаний, объединенных в общее пространство имен -
глобальное пространство модуля. Модули подключаются к программе (или другому
модулю) с помощью оператора import, после которого имена из пространства имен
модуля становятся доступными. Какие именно имена становятся доступны, определяет
оператор import: вариант import module делает доступным ровно одно имя -
имя модуля module, но зато через это имя можно использовать все глобальные имена
модуля в виде module.name. В варианте from module import name из
модуля импортируется указанное имя или список имен. В варианте
from module import * из модуля импортируются все имена. Хотя
автор модуля может ограничить этот список, а в отсутствии такого ограничения не
импортируются имена, начинающиеся с подчеркивания - считается, что это
внутренние имена модуля, не входящие в его публичный интерфейс.
Модуль может быть написан на Python, C или C++. Модули, написанные на Питоне,
позволяют создавать новые классы (об объектно-ориентированном программировании
речь будет идти ниже). Модули написанные на C и C++ позволяют создавать новые
типы данных. Модули, написанные на C/C++ могут быть встроенные (builtin) или
подгружаемые (DLL в Windows, разделяемые библиотеки в тех вариантах UNIX, в
которых формат выполняемых файлов ELF).
Модуль на Питоне - это текстовый файл с расширением .py, содержащий описания
переменных, функций и классов, плюс выполняемый код, который позволяет
инициализировать модуль. Этот код выполняется при первом импорте модуля, после
чего интерпретатор запоминает, что модуль уже проимпортирован и
проинициализирован, и при последующих импортах этого же модуля код инициализации
не выполняется.
Модули можно объединять в древовидные иерархии. Например, пакет XML содержит
в себе пакеты DOM, SAX, Parsers (и другие, в зависимости от реализации). В
результате можно проимпортировать PyExpat командой
import xml.parsers.expat, тогда команды этого модуля будут доступны как
xml.parsers.expat.ParserCreate, а можно проимпортировать его же командой
from xml.parsers import expat, тогда команды этого модуля будут
доступны как expat.ParserCreate. Или сразу
from xml.parsers.expat import ParserCreate!
Объектно-ориентированное программирование
Питон - объектно-ориентированный язык со множественным наследованием. Можно
сказать, что Питон поддерживает классическую ОО-модель с некоторыми
особенностями. Классы в Python могут иметь статические переменные, разделяемые
всеми экземплярами класса, но не могут иметь статических методов. Все методы
относятся к экземплярам класса. Все методы можно переопределять в наследниках.
Ссылка на объект (экземпляр класса) передается в методы в явном виде, в первом
параметре. Традиционно эту переменную называют self. Какого-то общего предка
всех классов (типа Object) в Python нет. Вообще в ОО-программировании в Питоне
важно не кто от кого наследуется, а какой поддерживается интерфейс; наследование
лишь дает реализацию. Формальных механизмов проверки интерфейсов пока нет, но
возможно они будут включены в язык и библиотеки; Zope делает
шаги в этом направлении.
Конструктор и деструктор класса называются __init__ и __del__ (встроенные и
служебные имена в Питоне обозначаются двумя подчеркиваниями перед и после имени;
это всего лишь соглашение, язык не запрещает программисту писать собственные
методы с такими именами). Вернее было бы назвать эти методы initializer и
finalizer - они сами не размещают и не освобождают память (это делает за них
интерпретатор), они инициализируют и очищают свои переменные.
В Питоне нет отдельного оператора new для создания экземпляров класса. Для
создания экземпляра класса вызывается класс с необходимыми параметрами. Эти
параметры передаются в __init__. Метод __del__, конечно, вызывается без
параметров (кроме, естественно, self). Для удаления объектов (и не только
экземпляров классов) в Питоне есть оператор del.
Пример.
class Foo:
bar = "baz"
def __init__(self, foo):
self.foo = foo
def __del__(self):
del self.foo
foo = Foo(12)
del foo
Описание класса создает новое пространство имен, в котором определяются
статические переменные (в нашем примере это bar) и методы. Создание экземпляра
порождает пространство имен объекта, доступ к которому осуществляется через
переменную экземпляра класса foo, а внутри методов класса - через переменную
self.
Классы в Питоне позволяют программисту создавать новые типы данных и
определять для них все операции, доступные для встроенных типов. Например, метод
__getitem__ позволяет индексировать объект, а __setitem__ - присваивать индексу
объекта. Метод __getitem__ также позволяет объекту участвовать в цикле for,
эмулируя последовательность (sequence). Есть методы, позволяющие объекту
эмулировать булевские значения и участвовать в операторах if и while. Методы
__getattr__ и __setattr__ позволяют читать и писать атрибуты объектов. Метод
__call__ позволяет вызывать экземпляр класса с параметрами!
Python позволяет переопределить все инфиксные операции, причем отдельно для
левого и правого аргумента выражения. Например, если a - экземпляр класса A, и b
- экземпляр класса B, то для вычисления выражения a + b Питон будет
сначала искать метод __add__ в классе A, а если не найдет - то метод __radd__ в
классе B (а если и там не найдет - возбудит исключение TypeError).
Многие программисты, особенно писавшие на C++, боятся и не любят
множественного наследования. Авторы языка Java вообще не включили множественное
наследование в язык. Совершенно напрасно! Python позволяет использовать
множественное наследование весьма успешно и удобно. Множественное наследование
облегчает переиспользование кода (code reuse) вместо
copy/paste-программирования, что очень важно и для эффективности, и для
читаемости программ, и для отладки. Часто программисты на Питоне создают класс с
помощью множественного наследования из нескольки связанных между собой
"e;кирпичиков"e;, словно из конструктора. Такие "e;кирпичики"e; в
ОО-программировании называются MixIn-классами. Подробную статью про
программирование с помощью MixIn-классов можно прочесть в Linux
Journal
Еще один способ использования классов (точнее, экземпляров), не связанный
непосредственно с ОО-программированием - использование пространства имен,
которое предоставляет объект. Рассмотрим следующую проблему. Вам надо пройти
циклом по списку, сохраняя между итерациями цикла некоторую информацию. Это
можно сделать циклом for, никаких проблем. А можно воспользоваться возможностями
функционального программирования, которые есть в Питоне - функциями map, filter,
reduce и тому подобное. Эти функции требую в качестве первого параметра функцию,
которую они в процессе цикла вызывают. Это эффективнее, чем цикл for (эти
функции-то написаны на C), но возникает проблема с хранением состояния между
итерациями. Функция, которую вызывает map может хранить состояние только в
глобальных переменных. Для простых программ это вполне приемлемо. Но вот,
скажем, с многопоточными программами будут проблемы - необходимо запирать и
синхронизировать доступ к глобальным переменным. Да и вообще к глобальным
переменным надо обращаться только при крайней нужде.
Вот тут на помощь приходит дополнительное пространство имен, существующее в
экземпляре класса. Создадим класс
class Process:
def __init__(self):
self.foo = 0
def __call__(self, v):
if self.foo > 100:
raise OverflowError
self.foo += v
return self.foo
, создадим экземпляр этого класса: p = Process(), и передадим
этот объект в map вместо функции: result = map(p, sequence).
Функция map, ничего не подозревая, будет вызывать переданный ей объект как
функцию с одним параметром. Никаких проблем - мы так описали класс, что его
экземпляры можно вызывать, и именно с одним параметром! И от итерации к итерации
объект p сохраняет необходимое состояние.
Другой похожий пример:
class Process:
def __init__(self):
self.sum = 0
def add(self, v):
self.sum += v
return self.sum
p = Process()
result = map(p.add, sequence)
print p.sum
Вся разница в этом примере - мы передаем не объект p, а его метод p.add.
Но что такое p.add? В Python это особая сущность, называемая BoundMethod. Это
объект, который помнит адрес объекта p, адрес функции add класса Process, и,
когда его вызывают, в свою очередь вызывает метод класса с правильным первым
параметром self. Если обратиться к этому методу как Process.add, то это -
UnboundMethod, и его надо вызывать, подставив все параметры в явном виде:
Process.add(p, 1). Вызов в таком виде часто используется для вызова
родительского конструктора или метода:
class Foo(Bar)
def __init__(self):
Bar.__init__(self)
Еще один вариант использования этого трюка - сортировка списков. Списки в
Питоне имеют метод sort(), который принимает параметр - функцию сравнения. Если
сравнение сложное, и зависит от внешних условий, в качестве функции можно
передать заранее проинициализированный объект.