div.main {margin-left: 20pt; margin-right: 20pt}Питон - модули, пакеты,
классы, экземпляры Олег
Бройтман
Это третья статья данного цикла лекций.
Перепечатка. Оригинал статьи опубликован по адресу http://www.softerra.ru/review/oses/linux/11349/
Модули - структурирование пространства имен
При создании больших программ или библиотек большим количеством
людей встает проблема коллизий имен. Питон решает эту проблему так
же, как и большинство современных языков - структурированием
пространства имен с помощью иерархически организованных модулей.
В Питоне 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-программирования, что очень важно и
для эффективности, и для читаемости программ, и для отладки. Часто
программисты на Питоне создают класс с помощью множественного
наследования из нескольки связанных между собой "кирпичиков", словно
из конструктора. Такие "кирпичики" в ОО-программировании называются
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(), который принимает параметр -
функцию сравнения. Если сравнение сложное, и зависит от внешних
условий, в качестве функции можно передать заранее
проинициализированный объект.
|