вторник, 30 ноября 2010 г.

Мультипоточность в питоне. Часть 2 - синхронизация

В части 1 мы рассмотрели базовые основы работы с потоками. Перейдем к примитивам синхронизации.

В мультипоточной программе доступ к объектам иногда нужно синхронизировать.

С этой точки зрения все объекты (переменные) разделяются на:

  • Неизменяемые. Мои самые любимые. Если объект никто не меняет, то синхронизация доступа ему не нужна. К сожалению, таких не очень много.
  • Локальные. Если объект не виден остальным потокам, то доступ к нему синхронизировать тоже не требуется.
  • Разделяемые и изменяемые. Синхронизация необходима.

Синхронизация доступа к объектам осуществляется с помощью объектов синхронизации (простите за тавтологию).

Объектов синхронизации существует множество - и постоянно придумывают новые. Это не то чтобы плохо - но избыточно (по крайней мере на начальном этапе изучения).

Каждый объект синхронизации имеет свои плюсы и минусы (обычно это баланс между быстродействием и областью применения). Ошибка ведет к тому, что

  • код будет выполнятся не так быстро, как мог бы (меньшее зло)
  • или к тому, что он будет выполнятся неправильно (тушите свет).

Давным-давно Дейкстра работал не только над алгоритмами на графах, но и над проблемами многопоточного кода. Ему удалось в одной из своих работ доказать, что минимальный базис, необходимый для любой синхронизации, сводится к двум объектам:

  • блокировки (они же lock, mutex).
  • условные переменные (condition variable).

Давайте на них и остановимся.

Ремарка о скорости. Питон - не первый спринтер на деревне. Если для программы на С выигрыш от правильного использования специфичного объекта синхронизации может быть существенным, то для Питона в целом они все примерно одинаковы.

Выбирать нужно более безопасный способ, не обращая внимания на быстродействие.

Кстати, это не относится к реализации CPython как такового - в коде интерпретатора присутствуют довольно экстравагантные блокировки, дающие выигрыш в пару процентов. До тех пор, пока их использует небольшая очень квалифицированная команда разработчиков - это оправдано.

Вторая ремарка: рассматриваемые объекты блокировки очень примитивны.

Откровенно говоря, не существует высокоуровневой теории мультипоточных программ. Если сравнивать с развитием языков, то уровень мультипоточного программирования сейчас находится где-то там, где были процедурные языки программирования. Объекты синхронизации схожи с оператором goto - использовать приходится, но всегда следует внимательно смотреть, что вы делаете.

Более высокоуровневые концепты, такие как monitor и active object хороши, но не решают всех проблем.

Языки вроде erlang, прекрасно справляющиеся с распараллеливанием задач на оптимальное количество потоков, останутся в своей узкой нише - примерно как lisp и все семейство языков функционального программирования навсегда остануться инструментом для маргиналов.

Парадигма многопоточных языков общего употребления все еще не создана.

Блокировки

Это - основа всего.

Простейший пример использования:

import threading

class Point(object):
    def __init__(self):
        self._mutex = threading.RLock()
        self._x = 0
        self._y = 0

    def get(self):
        with self._mutex:
            return (self._x, self._y)

    def set(self, x, y):
        with self._mutex:
            self._x = x
            self._x = y

Итак, имеем тривиальный класс. Это точка в двухмерном пространстве. Пусть координаты _x и _y начинаются с подчеркивания - чтобы программист, пожелавший менять их непосредственно немного задумался.

Еще есть _mutex типа RLock.

Публичная часть - два метода get и set.

Работает все это так: - при вызове метода берем блокировку через with self._mutex: - весь код внутри with блока будет выполнятся только в одном потоке. Другими словами, если два разных потока вызовут .get то пока первый поток не выйдет из блока второй будет его ждать - и только потом продолжит выполнение.

Зачем это все нужно? Координаты нужно менять одновременно - ведь точка это цельный объект. Если позволить одному потоку поменять x, а другой в это же время поправит y логика алгоритма может поломаться.

Есть и другой вопрос: зачем методу get блокировка? В приведенном примере она действительно не нужна. Но я всё же настоятельно рекомендую использовать блокировки даже для методов, вроде бы не изменяющих содержимое.

Во первых, это просто хорошая привычка. Иногда блокировки можно опускать для повышения производительности - но только после тщательного изучения побочных эффектов.

Во вторых (и это важнее) - блокировки при чтении позволяют корректно работать с объектами, части которых могут изменятся независимо.

Для примера возьмет цветную точку.

class ColoredPoint(Point):
    def __init__(self):
        super(ColoredPoint, self).__init__()
        self._color = 'green'

    @property
    def color(self):
        with self._mutex:
            return self._color

    @color.setter
    def color(self, val):
        with self._mutex:
            self._color = val

    def do(self, observer):
        with self._mutex:
            if self._color == 'red':
                observer(self.get())

Без блокировки в методе .do возможна ситуация, при которой один поток поменяет цвет, в второй в это же время изменит координаты. И тогда observer будет вызван с неправильными значениями.

В модуле threading существует еще и Lock - никогда его не используйте. Дело в том, что RLock (recursive lock) допускает повторную блокировку.

В примере для цветной точки это хорошо видно - .do берет блокировку, а затем вызывает метод .get, который берет эту блокировку еще раз.

В случае с Lock метод будет намертво повешен. RLock позволяет повторно брать блокировку тому потоку, который уже ее получил.

Говоря о блокировках, нужно упомянуть и две проблемы, которые возникают при их использовании:

  • Race condition: неправильное поведение из за отсутствия блокировки.
  • Dead lock: взаимная блокировка.

Первый случай я уже описал: при отсутствии блокировки один поток меняет часть объекта, временно приводя его в некорректное состояние - а второй поток именно в это время пытается работать с этим некорректным на данный момент объектом.

Пример для взаимных блокировок:

a = threading.RLock()
b = threading.RLock()

def f():
    with a:
        # do something
        with b:
            # do something also

def g():
    with b:
        # do something
        with a:
            # do something also

При одновременно вызове f из одного потока и g из другого оба потока навсегда повиснут: первый будет ждать захвата b а второй, захватив b остановится на ожидании блокировки a.

Обратите внимание: эта проблема может проявляться не сразу и нелегко диагностируется/воспроизводится.

Современные программы бывают большими и сложными с очень запутанными потоками исполнения, отследить которые нелегко.

Еще один пример:

def h():
    try:
        a.acquire()
        b.acquire()
    # do something
    finally:
        a.release()
        b.release()

При одновременном вызове h возможна взаимная блокировка.

Общее правило, немного помогающее при проектировании многопоточных систем: блокировки всегда должны браться в одном и том же порядке, а освобождаться в противоположном. Использование оператора with помогает решить эту проблему.

Условные переменные

С блокировками все более или менее ясно. Нужен еще один объект синхронизации.

Рассмотрим очередь на фиксированное количество элементов. Когда очередь пуста - поток, желающий получить новый элемент должен ждать. Аналогично с переполненной очередью.

Если решать все только на блокировках, придется проверять - а не пуста ли очередь? И если таки пуста - подождать еще немного. Сколько времени ждать - сложный вопрос.

Очевидно, нужен какой-то сигнал от заполняющего очередь потока - ожидающему.

Чтобы последний, выйдя из вечной блокировки мог продолжить работу.

Несмотря на то, что Питон непосредственно поддерживает сигналы (threading.Event) - я настоятельно не рекомендую их использовать.

Проблема в следующем: поток послал сигнал о поступлении элемента в очередь.

Кто его получит, если получение нового элемента ожидают несколько потоков?

Один из них забрал данные и очередь снова пуста. Как нужно дожидаться следующего поступления?

Условные переменные (condition variables) совмещают сигналы с блокировками.

Рассмотрим пример:

class Queue(object):
    def __init__(self, size=5):
        self._size = size
        self._queue = []
        self._mutex = threading.RLock()
        self._empty = threading.Condition(self._mutex)
        self._full = threading.Condition(self._mutex)

    def put(self, val):
        with self._full:
            while len(self._queue) >= self._size:
                self._full.wait()
            self._queue.append(val)
            self._empty.notify()

    def get(self):
        with self._empty:
            while len(self._queue) == 0:
                self._empty.wait()
            ret = self._queue.pop(0)
            self._full.notify()
            return ret

У нас два почти симметричных метода - положить в очередь и взять из нее.

Разберем .get. Сначала берем блокировку.

Обратите внимание - блокиратор один на обе условные переменные. Это важно Дело в том, что _full и _empty взаимозависимы. Хотя threading.Condition позволяет не указывать блокиратор, создавая новый RLock автоматически - не поступайте так. Можно поймать race condition, о котором я говорил раньше. Гораздо надежней и наглядней делать все явно. В нашем случае race condition был бы в явном виде - пусть и скрытый из за GIL.

  • Входим в блокиратор.
  • Проверяем наше условие - обычно это всегда цикл while.
  • Если условие не выполнилось - вызываем .wait(). Этот метод освободит блокировку и будет ждать извещения .notify. После получения сигнала выполнение продолжится при снова взятой блокировке. Еще раз, простыми словами.
    1. Проверка условия всегда выполняется в блокировке. Никто другой в это же время условие поменять не сможет (если все сделано правильно, конечно).
    2. Если условие не выполнилось - ждем опять, отдав управление операционной системе.
    3. При повторном вхождении у нас снова есть блокировка.
  • Условие выполнилось - очередь не пуста. Как минимум один элемент в ней есть. Может быть и больше - нас сейчас это не волнует. Блокировка все еще есть.
  • Получаем этот элемент и извещаем очередь, что она стала не до конца заполненной - т.е. в нее можно еще что-то положить (self._full.notify()).
  • Возвращаем полученное. Выходя из with освобождаем блокировку - другие потоки могут работать с очередью дальше.

Итак, условные переменные - это способ синхронизировать доступ к объектам при помощи блокировки и при этом возможность послать сигнал ожидающим потокам.

Альтернатива оператору with.

Вместо

with lock:
    # do something

можно использовать более традиционную форму:

try:
    lock.acquire()
    # do domething
finally:
    lock.release()

Эти два подхода абсолютно эквивалентны, но первый на три строчки короче. Выбросить try/finally блок нельзя - при возникновении исключения блокировка должна быть всё равно отпущена. К слову, неснятая блокировка при выходе из функции - одна из самых распространенных ошибок. Будьте внимательны - а еще лучше научитесь писать так, чтобы всё получалось просто, ясно и правильно.

Нужно отметить, что .acquire позволяет указывать параметры: blocking и, начиная с Python 3.2 - timeout. Подробности их использования неплохо описаны в документации.

Продолжение - в третьей части.

Мультипоточность в Питоне. Часть 1 - потоки

Продолжение - здесь

Писать многопоточные программы сложно. К сожалению, время от времени это приходится делать.

Ключ к созданию стабильно работающих мультипоточных програм лежит в соблюдении нескольких простых правил. В этой серии статей я попытаюсь их описать.

Для начала рассмотрим минимально необходимый базис:

  • работа с потоками
  • синхронизация разделяемых объектов (тех, которые одновременно используются в нескольких потоках)

Различные интересные вариации (неблокирующий доступ, невытесняющая многозадачность, распараллеливание на процессы) лежат несколько в стороне от "основной линии". Возможно, они будут рассмотрены в конце серии.

Потоки.

Потоки нужно

  • создавать
  • завершать
  • дожидаться завершения

Изначально у каждой программы есть один поток - он же "главный". Этот поток создается операционной системой при запуске процесса.

С точки зрения программиста почти не отличается от созданных вручную. Практически же существуют некоторые особенности.

Например, некоторые библиотеки для работы с графическим интерфейсом ожидают, что цикл обработки сообщений будет запущен в главном потоке, а не в каком-нибудь другом. То же самое относится и к высокоуровневым библиотекам для работы с сетью.

Создание потока.

Работой с потоком занимается threading.Thread (документация).

Меня захотят поправить - есть еще thread. Добрый совет - никогда его не используйте. Потому что!.. Цитата из документации: "The threading module provides an easier to use and higher-level threading API built on top of thread module."

Поток может быть создан двумя способами: передачей исполняемой функции в конструктор и наследованием.

Первый вариант:

def f(a, b, c):
    # do something
    pass

th = threading.Thread(name='th1', target=f, args=(1, 2), kwargs={'c': 3})
  • name - имя потока. Ни на что не влияет, но может быть полезно при отладке.
  • target - точка входа. Любой callable object - функция, связязанный метод класса или что-то еще
  • args - позиционные аргументы
  • kwargs - именованные аргументы.

Поток с именем 'th1' будет создан, но не запущен. После запуска будет вызвана функция f с параметрами a=1, b=2, c=3. Все аргументы могут быть опущены.

Второй вариант:

class MyThread(threading.Thread):
    def __init__(self, a, b, c):
        threading.Thread.__init__(self)
        self.a = a
        self.b = b
        self.c = c

    def run(self):
        # do something, using self.a, self.b, self.c
        pass

th = MyThread(1, 2, 3)

Результат практически тот же самый, но в новом потоке будет запущен метод run.

После того, как объект создан - его нужно запустить. В обоих случаях это делается через вызов

th1.start()

Все очень просто и понятно, верно?

Завершение потока.

Любой поток рано или поздно нужно завершить. Делается это простым выходом из функции потока. Не существует ПРАВИЛЬНОГО способа завершить поток снаружи. Это - принципиальное ограничение. Т.е. если вы хотите завершить поток из другого - просигнализируйте ему о своей просьбе (выставив флаг-переменную, например). В конце статьи я объясню, почему это важно.

Присоединение потока.

Культурные люди должны убирать за собой. Поэтому после сигнала о завершении нужно дождаться, когда поток реально закончит свою работу. Делается это так:

# signal to th1 for exiting
th1.join()

.join приостановит выполнение потока, вызвавшего ее, и будет ждать когда поток th1 завершит свое выполнение. Зачастую поток, стартовавший th1 его же и ждет - но бывают и исключения.

Почему нельзя "прибить" поток снаружи?

Если подумать, то все очень просто.

С точки зрения операционной системы программа (запущенный процесс) и потоки - ортогональные вещи.

Процесс владеет ресурсами - памятью, открытыми файлами, сокетами и подключениями к базе данных, окнами пользовательского интерфейса Типов ресурсов очень много. Грубо говоря, каждую переменную можно рассматривать как ресурс.

Поток - это нить исполнения. Он просто исполняет код параллельно с другими потоками. При этом поток ресурсами не владеет.

Открытый файл нужно закрыть, выделенную память вернуть обратно. Поэтому только правильно написанный код самого потока может корректно завершить самого себя.

Возникает еще один вопрос: а почему нельзя послать потоку исключение, чтобы он корректно свернулся - освободив при этом все что нужно?

Забегая вперед, вспомним об объектах синхронизации. Поток может быть в состоянии блокировки, когда его исполнение приостановлено в ожидании освобождения мьютекса (критической секции), занятого другим потоком.

Объекты блокировки - это в общем случае объекты ядра операционной системы. Т.е. блокированный поток нельзя "разблокировать", не затронув другие потоки.

Т.е. принудительное завершение потока - это потенциально опасная операция. В некоторых случаях все получается, иногда происходит утечка ресурсов.

Несмотря на то, что в низкоуровневом API операционной системы почти всегда есть способ аварийно завершить поток, в Питоне он не реализован.

Давайте просто писать "правильно", к чему нас и подталкивают.

Зачем нужно ждать завершения потока?

Хорошо, поток создан и запущен. В свое время ему послана просьба завершить работу. Зачем ждать подтверждения?

Нет, это не паранойя.

Во первых, так заведено в Питоне (корни растут из posix threads) - процесс завершается только тогда, когда все его потоки завершены.

Т.е. окончание работы главного потока программы еще ни о чем не говорит - Питон будет ждать окончания работы остальных потоков. Это выглядит так, будто в конце программы добавили .join на каждый работающий поток (еще одна причина делать это явно - ведь явное лучше неявного, верно?)

Откровенно говоря, есть лазейка, позволяющая это правило обойти. Достаточно указать

th1.setDaemon(True)

и можно о нем забыть. Отличие потока-демона от "стандартного" только в том, что демоны "не считаются" при завершении процесса.

Вуаля, решение найдено и оно работает? Не совсем.

Дело в том, что программа завершается не мгновенно. После того, как закончился главный поток с точки зрения программиста, Питон начинает чистить свои ресурсы, разрушая интерпретатор.

Закрываются файлы, уничтожаются модули.

И такой безобидный код как

import logging

def f():
    a = logging.getLogger("a.b.c").debug("f is called")

Может "поломаться", потому что модуль logging уже не существует, вернее - он частично разрушен сборщиком мусора.

При аккуратном программировании можно обойти все проблемы, связанные с саморазрушением интерпретатора - но оно вам нужно?

Проще и безопасней писать "по правилам".

Заключение.

С потоками покончено.

  • Потоки можно создавать и запускать.
  • Можно просить их закончить свою работу, но нельзя приказывать.
  • Завершения потока нужно дожидаться.

Но мультипоточное программирование только начинается.

Основная проблема здесь следующая: потоки должны взаимодействовать.

Или, другими словами, изменять состояние разделяемых между ними объектов. Несколько потоков могут пытаться изменить один объект одновременно, с непредсказуемым результатом.

Объекты могут быть сложными, и запись в объект "пользователь" фамилии одним потоком, а имени другим может привести к неожиданному результату. Существуют и более "разрушительные" примеры.

Для решения проблемы придуманы объекты синхронизации. О них - в следующей статье.

четверг, 27 мая 2010 г.

Питон: импорт и модули - часть 3

Питон: импорт и модули - часть 3.

Начало - в первой и второй частях.

Поговорим о расширениях импорта (import hooks) - sys.meta_path, sys.path_hooks, sys.path_import_cache

Зачем они вообще появились?

Все очень просто. Захотелось добавить поддержку импорта из .zip архивов. Ява такое может (.jar) - чем Питон хуже?

Разразилась довольно бурная дискуссия, в ходе которой стало ясно - если уж расширять, то делать это хорошо, чтобы сразу убить одним выстрелом всех зайцев и оставить сестрам по серьгам.

Например, уметь загружать модули из базы данных или с соседнего сервера.

Так появился PEP 302 - New Import Hooks

Думаю, все получилось неплохо.

Это

  • искатель (finder)
  • загрузчик (loader)
  • sys.meta_path
  • sys.path_hooks
  • sys.path_import_cache

В этой статье я хочу разобрать перечисленные пункты, оставив написание нетривиальных расширений на следующий раз. Тема довольно сложная, и изложить все быстро - нелегко.

Найти и загрузить

На самом деле импорт довольно естественно раскладывается на две части:

  • найти модуль по имени, получив в результате объект - загрузчик.
  • загрузить этот модуль

Соответственно имеем два контракта:

class Finder:
    def find_module(self, fullname:str, path:[str]=None) -> Loader:
        pass

class Loader:
    def load_module(self, fullname:str) -> types.ModuleType:
        pass

Искатель имеет единственный метод .find_module, который принимает полное имя модуля и необязательный путь. Путь сейчас неинтересен, а на имени остановимся более подробно.

Хочу подчеркнуть, что это именно полное имя модуля. Т.е. __import__ сначала превратит относительное имя в абсолютное, если нужно - и только затем начнет работать с искателями и загрузчиками.

Если модуль найден - возвращается экземпляр загрузчика для этого модуля (у которого будет вызван .load_module вторым шагом). Если искатель не может найти модуль с запрашиваемым именем (например, потому что с этим именем работает другое расширение импорта) - следует вернуть None. Выброшенное исключение приведет к ImportError.

Загрузчик должен иметь метод .load_module, который проводит всю загрузку. Есть еще несколько необязательных методов, применяемых для "продвинутых" загрузчиков.

Мы подробно рассмотрим искатель и загрузчик в следующей статье. Сейчас нужно запомнить следующее:

  • импорт модулей выполняется в два шага
  • все начинается с искателей, которые возвращают загрузчики для тех полных имен модулей, которые эти искатели умеют обрабатывать.
  • механизм импорта сам умеет построить полное имя модуля до вызова расширений. Т.е. он правильно преобразует относительный импорт в абсолютный, и попросит найти модуль по его абсолютному имени. Более того, он обеспечит загрузку пакета перед тем, как перейти к модулю в нем.
  • имена модулей (как абсолютные так и относительные) не имеют никакого отношения к файловой системе. Да, часто они похожи - потому что Питон использует файловую систему для хранения своих файлов. Но с точки зрения расширений импорта это - совершеннно разные вещи.

Я вообще считаю, что употребление терминов ".py файл" и "папка с __init__.py" в большинстве случаев некорректно. Используйте "модуль" и "пакет", объединяя последние в "библиотеки".

Регистрация искателей.

Очевидно, что искатели должны быть сначала где-то зарегистрированы перед использованием.

Таких мест два:

  • sys.meta_path
  • sys.path_hooks (в связке с sys.path_import_cache)

sys.meta_path

Содержит список экземпляров искателей. По умолчанию пустой.

sys.path_hooks

Содержит список фабрик искателей. (Класс - фабрика для его экземпляров). По умолчанию в нем зарегистрирован импортер для zip файлов.

>>> import sys
>>> sys.path_hooks
[<type 'zipimport.zipimporter'>]

Схема работы расширений импорта

Здесь нужно сделать оговорку. Сначала опишу, как все задумывалось.

Процесс "ввода в строй" PEP 302 несколько затянулся. Т.е. полностью поддерживается вся спецификация, но унификация расширений и старого механизма импорта не выполнена.

В результате имеем как бы две подсистемы - сначала пытаемся задействовать расширения. Если они не подошли - переходим к импорту "по старинке".

Это неудобно разработчикам Питона (частичное дублирование функционала и трудность поддержки двух систем одновременно).

Еще более неудобно создателям расширений - в стандартной библиотеке просто напрочь отсутствует масса необходимых утилитных функций, их приходится создавать каждый раз заново.

На пути к окончательному решению вопроса стоит два препятствия:

  • огромный объем работ, требуемых для переписывания той вермишели, которая уже присутствует в Python/import.c. Плюс нужно все тщательно протестировать, чтобы не нарушить ни один пункт текущего поведения. Блок юниттестов большой, но не совсем полный. К тому же есть довольно значительная часть платформозависимого кода (Linux, Windows, MacOS, OS/2). Ее тестировать еще сложнее
  • опасения по деградации производительности. Новый код должен быть не медленней старого

Есть и довольно существенный стимул помимо красоты единого дизайна - в Python 3 введена поддержка идентификаторов на родном языке пользователя (т.е. можно создавать переменную Вася_Пупкин = True). Модули, конечно, тоже можно называть по русски. Так вот, сейчас все более или менее работает, но на ряде граничных условий таки ломается (редко, такой случай действительно трудно поймать). Закрыть все ошибки можно только переписав большую часть кода. Когда-нибудь (Python 3.3?) все починится.

Изложение всех хитросплетений сбивает с толку.

Поэтому рассмотрим сначала "идеальную картинку".

Импорт - как оно должно быть

Импорт начинается с sys.meta_path (список - упорядоченная коллекция).

В нем находятся экземпляры искателей. Обрабатываем их по очереди.

Первый искатель, у которого .find_module вернет не None - наш (по аналогии с обработкой sys.path). Сам sys.path на этом уровне пока не участвует.

Возвращенный объект - загрузчик. Вызываем у него .load_module и получаем наш модуль. Загрузчик сам должен зарегистрировать его в sys.modules. Все, модуль загружен.

package_name = fullname.rpartition('.')[0]
path = None
if package_name:
    # fullname имеет форму 'a.b.c'
    # package_name равен 'a.b'
    # в sys.modules уже есть пакет 'a.b'
    # и его __path__ обязан быть установлен
    # path необходим - помните, в первой статье из серии
    # был пример пакета, который разбросан по нескольким
    # путям 
    path = getattr(sys.modules[package_name].__path__)

for finder in sys.meta_path:
    loader = finder.find_module(fullname, path)
    if loader is not None:
        module = loader.load_module(fullname)
        return module

После инициализации интерпретатора sys.meta_path уже должен содержать как минимум один элемент. Назовем его StandardFinder.

Этот "стандартный искатель" обрабатывает оставшуюся часть механизма импорта. Лучше, если он будет последним элементом списка - чтобы больше походить на текущую реализацию (но, строго говоря, это не обязательно).

Стандартный искатель в свою очередь пройдет по списку sys.path_hooks. Для каждого элемента sys.path он попробует создать "искатель второго рода" (используя sys.path_import_cache для ускорения работы). Если вложенный искатель отзовется на свой .find_module - будет использоваться возвращенный им загрузчик.

class StandardFinder:
    def find_module(self, fullname, path=None):
        if path is None:
            # путь не указан, используем sys.path
            path = sys.path

        for elem in path:
            # стандартный искатель ожидает, что элементы -
            # пути в файловой системе 
            # (с небольшими вариациями как у zip архивов)
            # для каждого элемента из предложенных
            # сначала пробуем кеш
            finder = sys.path_import_cache.get(elem)
            if finder is None:
                # в кеше ничего не найдено
                for finder_class in sys.path_hooks:
                    try:
                         # пытаемся создать искатель для элемента
                         finder = finder_class(elem)
                    except ImportError:
                         # это нормально, зарегистрированный
                         # искатель второго рода не умеет обрабатывать
                         # предложенный элемент
                         # попробуем следующего кандидата
                         continue
                    else:
                         # вторичный искатель найден,
                         # зарегистрируем его в кеше для дальнейшего
                         # использования
                         sys.path_hooks[elem] = finder
                         break
                if finder is not None:
                    break

        if finder is not None:
            # как бы то ни было, вторичный искатель нашелся
            loader = finder.find_module(fullname)
            if loader is not None:
                # и, о счастье, он готов предоставить загрузчик
                # его и вернем
                return loader
        # никто не подошел, 
        # даем шанс системе импорта найти следующего кандидата
        return None

У стандартного искателя код получился более замысловатым. Обратите внимание:

  • в sys.path_hooks хранятся фабрики (класс ведь тоже фабрика для создания экземпляров).
  • фабрика вызывается со строкой - элементом, похожим на файловый путь (здесь может быть уместно применение URI/URL, если удобно). Если фабрика готова работать с элементом - она выбрана и следующие кандидаты не рассматриваются. Косвенным подтверждением данного факта является sys.path_import_cache - словарь элементов на искатели второго уровня.
  • У выбранного искателя второго рода .find_module вызывается без параметра path - он уже был передан в конструктор.

Естественно, чтобы эта схема заработала нужно в sys.path_hooks при инициализации зарегистрировать еще один вторичный искатель помимо одинокого zipimporter, который бы обрабатывал старые добрые пакеты и модули, лежащие просто в файловой системе (StandardFileSystemFinder). Также он должен поддерживать импорт C Extensions (все равно zipimporter без дополнительных ухищрений этого не умеет).

Текущая ситуация

Состояние дел на сегодняшний момент внешне не сильно отличается от предложенной схемы.

Конечно, StandardFinder и StandardFilesystemFinder не существуют и нигде не зарегистрированы.

Просто подразумевайте, что StandardFinder неявно стоит в конце sys.meta_path, а StandardFilesystemFinder в конце sys.path_hooks.

В sys.path_import_cache StandardFilesystemFinder тоже неявно присутствует.

Я сделал файл a.zip со следующим содержимым:

  • a.zip
    • a
      • __init__.py
      • b.py

Смотрите, что вышло:

>>> import sys
>>> sys.path.append('./a.zip')
>>> import a
>>> a.__path__
['./a.zip/a']
>>> import a.b
>>> from xml import dom
>>> sys.path_importer_cache
{
 ...
 './a.zip/a': <zipimporter "./a.zip/a/">,
 './a.zip': <zipimporter "./a.zip">,
 '.../py3k/Lib/xml/dom': None
 '.../py3k/Lib/xml': None
 ...
}

Вместо гипотетического StandardFilesystemFinder в sys.path_importer_cache проставляется None - не стоит пытаться обнаружить искатель для того файлового пути, которому уже не подошел ни один зарегистрированный кандидат.

В остальном наша "идеальная картинка" совпала с существующим интерпретатором.

Заключение

Мы весьма подробно рассмотрели то, как Питон работает с расширениями импорта.

Несмотря на то, что я иногда использовал аннотации из третьего Питона - все обратно совместимо до версии 2.3, в которой PEP 302 был впервые реализован.

Важно понимать различия между sys.meta_path и sys.path_hooks. При всей внешней схожести применяемых интерфейсов они служат разным целям.

К сожалению, спецификация PEP 302 уделяет этому недостаточное внимание. Общий совет такой:

  • если ваши расширения хорошо укладываются на файловую систему (zip архивы выглядят классическим примером) - используйте sys.path_hooks
  • в противном случае следует работать с sys.meta_path

В целом рекомендация правильная - но мне было очень нелегко понять разницу между первым и вторым подходом. Ведь практически в любом случае можно так или иначе построить соответствие "имя модуля" -> "уникальная строка", по которой можно загрузить этот модуль. Например, для базы данных это может быть sqlite:///<path to database>.

Различие лежит глубже. Для доступа к базе данных требуется открыть соединение к ней. Если повторное открытие архива на чтение - довольно необременительная операция (файл скорее всего уже в кеше операционной системы и, значит, в оперативной памяти), то для базы данных каждое новое соединение довольно накладно.

С другой стороны создание искателей первого рода, зарегистрированных в sys.meta_path требует большей внимательности к деталям того же кеширования - нельзя воспользоваться уже готовым sys.path_importer_cache.

Поэтому я попытался дать максимально подробное описание того, как это все работает. Надеюсь, вышеизложенное поможет сделать правильный выбор - или подскажет момент, когда следует переключиться на альтернативную схему.

В следующей части будут столь же скурпулезно разобраны искатель и загрузчик (Finder и Loader) с созданием элементарного примера - и будет дан беглый обзор замечательной стандартной библиотеки importlib (3.1+)

И, конечно, где-то впереди нетривиальная "конфетка" - демонстрация по настоящему сложной элегантной системы, которую невозможно построить без описываемого механизма.

вторник, 25 мая 2010 г.

Импорт в Питоне - часть 2

Питон: импорт и модули - часть 2.

Первая часть - здесь.

Откуда грузятся модули?

Простой ответ - из sys.path - будет лишь отчасти верным.

Полный список должен содержать:

  • __import__
  • sys.modules
  • sys.path
  • .pth файлы
  • sys.meta_path
  • sys.path_hooks
  • sys.path_import_cache

Давайте рассмотрим все по порядку.

__import__

Любая форма импорта, а их с точки зрения CPython три:

import a.b
from a.b import c
from a.b import *

сводится к вызову функции __import__(name:str, globals:dict={}, locals:dict={}, form_list:list=[], level:int=-1) -> types.ModuleType

  • name - имя модуля. С точками внутри, если нужно.
  • globals - глобальное пространство имен блока, который загружает модуль. О пространствах имен я здесь рассказывать не буду - тема для отдельной большой статьи. Упомяну лишь, что его можно получить через вызов globals()
  • locals - локальное пространство имен, locals(). При импорте не используется.
  • from_list - список имен, которые нужно получить из импортируемого модуля.
  • level - уровень вложенности. Используется для относительного импорта.

Честно говоря, эта функция весьма запутана и ее использование довольно нетривиально. По ней можно отследить развитие механизма импорта в Питоне - модули, имена из модулей, относительный импорт. При разном наборе параметров она дает различный результат. Не люблю.

Описывать подробно все не хочу - читайте стандартную документацию.

Замечу, немного забегая вперед - начиная с Python 3.1 появился замечательный пакет importlib, в котором есть удобная importlib.import_module.

__import__ реализован в Python/import.c - довольно большой файл.

Что он делает?

  • блокировки потоков.
  • работа с модулями
  • поддержка пакетов
  • импорт модулей, написанных на Питоне
  • поддержка C Extensions
  • работа с кешем питоновских модулей (.pyc файлы)
  • встроенные (builtins) и замороженные (frozen) модули. Последние могут быть интересны для разработчиков систем, в которые Питон вшит внутрь (python embedding)
  • расширения (import hooks)
  • платформозависимый код - на linux все выглядит немного иначе, чем на Windows. А в import.c еще есть код, специфичный для MacOS и OS/2.

Следует упомянуть еще один стандартный модуль - imp. Он содержит набор низкоуровневых функций, необходимых для работы импорта. Я не буду давать его полное описание - но в дальнейшем упомяну ряд интересных функций.

При изучении внутренней реализации следует всегда помнить, что импорт развивался долго и трудно. Нововведения обязаны были поддерживать обратную совместимость с уже имеющимся кодом. Текущая картина представляет результат этого компромисса.

При этом часть заложенных возможностей все еще не реализована - особенно это касается расширений, которые хоть и стали стандартным механизмом начиная с Python 2.3, все еще выглядят как сторонняя надстройка. Когда-нибудь весь импорт будет построен на расширениях (Python 3.3?)

Как бы то ни было, в изложении я буду описывать текущее состояние дел, показывая где нужно "идеальную картинку", к которой все рано или поздно придет.

Я буду в основном говорить о модулях, написанных на Питоне .py файлах, если отдельно не будет упомянуто. C Extensions - отдельная очень интересная тема, о которой можно писать очень долго.

Преамбула закончена. Приступим к детальному рассмотрению.

Блокировка

Импорт модуля изменяет глобальные переменные (в первую очередь sys.modules. Чтобы избежать возможные накладки, получающиеся при параллельной загрузке модулей из разных потоков, используется блокировка.

В модуле imp для этого существуют три функции:

  • imp.acquire_lock() - взять блокировку
  • imp.release_lock() - отдать ее
  • imp.lock_held() - проверить, взята ли?

Это выглядит так: первым делом __import__ берет блокировку, затем загружает модуль.

Модулей может быть несколько: помните - import a.b.c превращается в

import a
import a.b
import a.b.c

Т.е. при загрузке модуля из пакета сначала загружаются модули верхних уровней. При этом каждый модуль может содержать другие импорты, которые будут выполнены.

Строго говоря, этот процесс выглядит так:

  • создать модуль
  • создать код для этого модуля (преобразовать питоновский текст в байт-код)
  • выполнить этот байт-код в глобальном пространстве имен модуля. Подробности - в следующих статьях этой серии.

После загрузки модуля блокировка снимается.

Мне никогда не приходилось работать с функциями блокировки напрямую. Импорт сам делает все, что нужно, но знать о блокировках необходимо.

Обычно модули импортируются как first level statemets в начале вашего файла.

Но инструкцию импорта можно писать и внутри функции.

def f():
    from twisted.internet import reacor
    reactor.callLater(0, lambda: None)

Это делается для отложенной загрузки. Например, для того чтобы разорвать циклическую зависимость модулей. Или, как в случае с twisted, работать с реактором только после того, как был выбран его тип (select, poll, epoll и т.д.)

И все выглядит прекрасно, если ваши функции с import statement внутри работают в одном потоке - лучше всего в главном.

В случае многопоточной программы, интенсивно использующей свои блокировки, можно все подвесить - так называемый dead lock. Я встречал такую ситуацию в моей практике пару раз. Мультипоточность не является темой данной серии статей. Если вы пишите такую программу, то должны знать о взаимных блокировках. Пожалуйста, учитывайте при разработке еще и блокировку импорта.

Памятка: импорт уже загруженного модуля быстрый. Но не мгновенный - Питон возьмет блокировку проделает несколько операций, прежде чем вернуть уже загруженный модуль.

sys.modules{str: types.ModuleType}

Словарь уже загруженных в Питон модулей.

Давайте глянем на него подробней.

>>> import pprint, sys
>>> pprint.pprint(sys.modules)
{
...
 'StringIO': <module 'StringIO' from '/usr/lib/python2.6/StringIO.pyc'>,
 'UserDict': <module 'UserDict' from '/usr/lib/python2.6/UserDict.pyc'>,
 '_ctypes': <module '_ctypes' from '/usr/lib/python2.6/lib-dynload/_ctypes.so'>,
 '__builtin__': <module '__builtin__' (built-in)>,
 '__main__': <module '__main__' from '/usr/bin/bpython'>,
 'ctypes': <module 'ctypes' from '/usr/lib/python2.6/ctypes/__init__.pyc'>,
 'ctypes._endian': <module 'ctypes._endian' from '/usr/lib/python2.6/ctypes/_endian.pyc'>,
 'encodings': <module 'encodings' from '/usr/lib/python2.6/encodings/__init__.pyc'>,
 'encodings.aliases': <module 'encodings.aliases' from '/usr/lib/python2.6/encodings/aliases.pyc'>,
 'encodings.utf_8': <module 'encodings.utf_8' from '/usr/lib/python2.6/encodings/utf_8.pyc'>,
 'sys': <module 'sys' (built-in)>,
 'zipimport': <module 'zipimport' (built-in)>,
...
}

Конечно, я пожалел читателя и нещадно порезал ту бесконечность, которую питон должен загрузить перед началом своей работы.

Итак, что мы видим.

  • builtins - встроенные модули, у которых отсутствует имя файла:
    • __builtin__
    • обязательный __main__ (это ваш файл, с которым вы запустили python)
    • sys - много вкусного
    • zipimport - для загрузки модулей, хранящихся в zip архивах
  • C Extensions - расширения, написанные на языке С и не только
    • _ctypes, указывающий на _ctypes.so
  • модули верхнего уровня StringIO и UserDict
  • пакеты ctypes и encodings с вложенными модулями

Импорт складывает загруженные модули в sys.modules.

Еще раз подчеркну: если модуль уже там лежит - он быстро возвращается (но блокировка все равно берется).

Импорт: абсолютный, относительный и непонятный

Технически есть два вида: абсолютный и относительный.

  • При абсолютном следует указывать имя модуля начиная с самого верха: import a.b.c
  • Потом появился относительный (2.5+):

    from . import c

Наверное, уже все успели с ним познакомится. На самом деле очень удобно: точка означает папку, в которой лежит импортирующий модуль. Две точки подряд - прыжок на уровень выше.

Именно для этого появился последний параметр level в __import__: он показывает, на сколько уровней вверх нужно заглянуть, чтобы загрузить name.

К сожалению, и тут не все гладко. В старых (до 2.5) питонах относительный импортов не было. Поэтому при import os питон сначала пытался загрузить os.py в той папке, где находился вызывающий модуль. Если файла не нашлось (а чаще всего так и бывает), то питон будет искать модуль по абсолютному пути. А чтобы не обращаться к файловой системе опять (время дорого) - в sys.modules вставится заглушка:

>>> pprint.pprint(sys.modules)
{ 
 ...
 'encodings': <module 'encodings' from '/usr/lib/python2.6/encodings/__init__.pyc'>,
 'encodings.__builtin__': None,
 'encodings.aliases': <module 'encodings.aliases' from '/usr/lib/python2.6/encodings/aliases.pyc'>,
 'encodings.codecs': None,
 'encodings.utf_8': <module 'encodings.utf_8' from '/usr/lib/python2.6/encodings/utf_8.pyc'>,
 ...
}

Обратите внимание: encodings.__builtin__ и encodings.codecs указывают на None. Это значит, что питон будет при следующей попытке искать __builtin__ и codecs по абсолютному пути.

Добавлю, что начиная с 2.7+ "компромиссный" способ невозможен. Пишите либо полный путь, либо указывайте его явно с точки. И это замечательно!

sys.path:[str]

Начиная разговор о том, где Питон находит новые модули, невозможно пропустить sys.path. Все с него начинается и часто им же и заканчивается.

sys.path представляет собой список файловых путей, в которых лежат модули.

>>> import sys
>>> import pprint
>>> pprint.pprint(sys.path)
[
 '.',
 '/usr/local/lib/python2.6/dist-packages/distribute-0.6.10-py2.6.egg',
 '/home/andrew/projects/reaction',
 '/usr/local/lib/python2.6/dist-packages/rpyc-3.0.7-py2.6.egg',
 '/usr/lib/python2.6',
 '/usr/lib/python2.6/plat-linux2',
 '/usr/lib/python2.6/lib-tk',
 '/usr/lib/python2.6/lib-old',
 '/usr/lib/python2.6/lib-dynload',
 '/usr/lib/python2.6/dist-packages',
 '/usr/lib/python2.6/dist-packages/PIL',
 '/usr/local/lib/python2.6/dist-packages',
 ...
]

Как видим, сюда попадает прежде всего сам питон, установленные библиотеки и мои собственные проекты.

Поиск модуля ведется с начала списка, и не случайно первой стоит точка (текущая папка). Модуль из текущей папки загрузится первым, перекрыв остальные.

Поэтому не пытайтесь создавать свои модули с именами pickle или urllib - они перекроют стандартные и вы получите странную ошибку при импорте.

sys.path можно изменять из питоновского кода, чтобы подключить ваши модули и пакеты.

Крайне не советую это делать - лучше писать distutils скрипт setup.py, который установит вашу чудесную библиотеку в питон.

Конечно, меня сразу же поправят - делать distutils неудобно. Согласен, используйте distribute, setuptools, paver, enstaller - что вам больше по душе.

По этому поводу написано немало статей, а мы все же рассматриваем сейчас немного другой вопрос. Последние два года Тарик Зиаде интенсивно занимается переписыванием distutils с целью учесть все недостатки и создать по настоящему замечательную штуку. Удачи ему.

Как бы то ни было, нужно понимать способ, которым наполняется sys.path.

В первую очередь питон добавляет текущую папку и стандартную библиотеку (папка Lib, если смотреть на питоновские исходники).

Затем следует импорт site.py.

site.py

Предназначен для настройки Питона. Большая часть файла занимается добавлением путей в sys.path. Не поленитесь, откройте его в текстовом редакторе и рассмотрите. Это не больно.

Чтобы узнать, где он лежит - сделайте

>>> import site
>>> site.__file__
'/usr/lib/python3.1/site.py'

На первый взгляд содержимое представляет дикую мешанину из различных способов расширения. На второй взгляд - тоже. Что поделать - цена обратной совместимости и отражение развития представлений об импорте.

При этом поставщики различных дистрибутивов могут немного подкручивать его содержимое. Особенно этим славятся Debian и Ubuntu. Использую - но плАчу, как тот ёжик.

Позвольте мне остановится на "минимальном стандартном наборе", а все многочисленные тонкости изучайте сами.

Итак, это в первую очередь site-packages - обычно папка внутри стандартной библиотеки питона. Сюда устанавливаются сторонние библиотеки, которые не поставляются вместе с питоном.

Начиная с Python 2.6 поддерживаются еще и локальные пользовательские папки: ~/.local/lib/python2.6/site-packages или %APPDATA%/Python/Python26/site-packages для Windows.

Для детального изучения читайте PEP 370: Per-user site-packages Directory и внимательно изучайте ваш site.py. Дело в том, что для новых версий схема может быть иной - ~/.local/lib/python.3.1/site-packages. Различия, впрочем, невелики.

Более интересны так называемые .pth файлы, которые могут содержаться в site-packages.

Дело в том, что сторонние пакеты могут иметь разную структуру.

Например,

  • dpkt-1.6
    • AUTHORS
    • CHANGES
    • README
    • dpkt
      • __init__.py
      • dpkt.py
      • dhcp.py
    • examples
      • example-1.py
    • tests
      • test-perf.py
    • setup.py

Для import dpkt нужна папка dpkt-1.6, в которой уже есть пакет dpkt с __init__.py внутри. Поддерживать два дерева каталогов "для разработки" и "для питона" неудобно.

Поэтому можно положить в site-packages файл dpkt.pth, содержащий путь к папке, внутри которой будет питоновский пакет dpkt.

site.py пройдется по всем .pth файлам и обработает их.

Обработка в данном случае заключается в следующем:

  • все строки, начинающиеся с # - комментарии
  • строка, начинающаяся с import должна быть исполнена. После точки с запятой, отделяющих новую команду - можно писать любой код. Грязный хак, облегчающий жизнь в некоторых ситуациях
  • все прочие строки добавляются в sys.path

Обратите внимание - путь может указывать куда угодно, в том числе и на вашу папку, в которой вы держите рабочие проекты.

Подчеркну, еще раз, что создавать самому .pth файлы - моветон.

Делайте правильные setup.py, используйте distribute, регистрируйте разрабатываемые вами библиотеки через python setup.py develop. Еще лучше применяйте при этом virtualenv.

Я рассказал о .pth файлах только в рамках общего обзора импорта модулей.

Последним шагом site.py делает import sitecustomize. sitecustomize.py обычно кладут в ту же папку, где расположен запускаемый питоновский скрипт. Это позволяет настроить интерпретатор перед запуском кода этого скрипта (подкрутить тот же sys.path к примеру).

Никогда так не делайте. При правильной организации проекта такой трюк не нужен. Зато я видел много проблем у тех, кто пытался использовать sitecustomize. Не хочу подробно на них останавливаться - и так много негативных посылов в этой части. Будут просьбы - расскажу все очень подробно на предметном материале.

Импорт и главный модуль.

Не могу обойти вниманием __main__.py. Так называется модуль, который вы непосредственно запускаете через python <script.py>.

Также в конце этого модуля считается правилом хорошего тона писать

if __name__ == '__main__'
    main()

чтобы вызвать функцию main только тогда, когда файл используется как скрипт.

На самом деле, конечно, можете писать и вызывать что угодно.

Смысл этого блока в том, чтобы делать вызов main() только тогда, когда мы запускаем скрипт непосредственно (из командной строки, кликая по нему мышкой и т.д.)

Обычно это ведет к разбору аргументов командной строки и отработке программы (выводу на консоль, созданию окошек GUI и прочее).

Если этот модуль был загружен из другого скрипта, то все эти побочные действия ни к чему - нужно получить сам модуль и работать с его объектами (функциями, переменными, классами). В этом случае имя модуля будет другим (__main__ указывает на вызывающий скрипт).

Есть несколько способов запустить скрипт:

  • указать его явно в командной строке. Тривиально.
  • написать python -m unittest . (2.4+) - в данном случае запустить юниттесты для нашей папки, в которой лежат тестовые сценарии.

Последний механизм подправляли в 2.5 и 2.6:

Наиболее интересен последний PEP. Дело в том, что 2.5 стал поддерживать относительные пути импорта (которые начинаются с точки). Но __main__ - модуль верхнего уровня. "Выше" быть ничего не может а "рядом" лежат модули из стандартной библиотеки.

Поэтому в 2.6 ввели атрибут модуля __package__:

if __name__ == "__main__" and __package__ is None:
    __package__ = "expected.package.name"

Теперь можно указать свой пакет, если модуль выполняется как скрипт.

Последняя малоизвестная часть относится к импорту из zip архивов PEP 273: Import Modules from Zip Archives

Если вы положите файл с именем __main__.py в такой архив, то можно запустить его через python <achive.zip>.

Я еще раз призываю строить разработку основываясь на библиотеках, пакетах и модулях, а не на файлах, папках и архивах. Разница довольно тонкая, но очень существенная.

Тем не менее могут быть случаи, системному администратору удобно использовать именно этот подход:

  • его "скриптик" вырос и не помещается в один .py файл.
  • тем не менее он еще не дорос до "большой библиотеки" со всем полагающимся оформлением.

Заключение

За рамками статьи остается PEP 382: Namespace Packages и много интересных особенностей, относящихся к sys.path.

К сожалению эта тема настолько обширна и запутана, что я просто не в силах рассказать обо всем сразу.

Следующая статья из серии будет посвящена беглому обзору того, как Питон обрабатывает расширения импорта (знаменитый PEP 302).

И только потом я смогу перейти (наконец-то!!!) к собственно разговору о том, как писать import hooks и зачем они могут быть нужны "простому программисту".

Продолжение - в следующей части.

суббота, 22 мая 2010 г.

Импорт в Питоне

Питон: импорт и модули - часть 1.

Любая программа использует импорт модулей. Но, как показал беглый опрос, далеко не все программисты представляют, как работает импорт, что содержится в модуле и как этот импорт можно расширять для своих нужд.

Удивительный факт: даже англоязычные ресурсы предоставляют недостаточно информации.

Эта серия статей попытается заполнить образовавшийся пробел.

В изложении я буду опираться на Python Enhancement Proposals и исходный код CPython. Рассматривается только CPython, он же "Питон как таковой". Возможно, Jython, IronPython, PyPy имеют отличия - я серьезно с ними не работал и, по большому счету, эти замечательные штуки меня мало интересуют).

Механизм импорта постоянно (хотя и не очень быстро) развивается, поэтому в необходимых случаях будут приводится номера версий Питона, после которых стало доступно то или иное расширение. Большая часть изложенного подойдет для ветки 2.х - но, конечно, мне трудно удержаться от упоминания всех "вкусностей", которые появились (и продолжают вводится) в py3k.

Отдельно нужно сказать о Python 3.2. На момент публикации эта ветка находится в стадии разработки. Доступна как python 3.2. Нестабильна и все еще может довольно значительно поменяться.

Введение закончено. Приступим к делу.

Определения

  • модуль (module) - базовое понятие языка. Содержит код и глобальные переменные. Исполняется при загрузке. Модули бывают написанные на питоне (.py файлы), C Extensions (.pyd и .so файлы, поддерживающие определенный интерфейс) и встроенные в интерпретатор (технически выполненные как C Extensions). Об определении модуля можно писать долго, но этот объект хорошо знаком даже начинающим программистам. Замнем для ясности.
  • пакет (package) - разновидность модуля, используемая для собирания модулей в иерархическую древовидную структуру. Классические папки с __init__.py внутри.

При

import a.b.c

будут последовательно выполнены

import a
import a.b
import a.b.c

Это нужно знать.

Стандартные атрибуты модуля

Строки вроде

import os
from xml import dom

импортируют модуль (os и xml.dom соответственно) и добавляют os и dom в текущее пространство имен.

os и dom - объекты типа "модуль":

>>> os
<module 'os' from '/usr/lib/python2.6/os.pyc'>

У каждого модуля есть набор специфических атрибутов:

  • __name__: str - полное имя модуля. Путь от начала с точками как разделителями. Например, 'xml.dom' или 'xml.dom.minidom'
  • __doc__: str - описание (так называемый docstring)
  • __file__: str - полный путь к файлу, из которого модуль был создан (загружен). До версии Python 3.2 это путь к .py или .pyc (записанный на диск кеш, результат автоматической компиляции кода модуля, используется для ускорения загрузки) - '/usr/lib/python2.6/xml/dom/__init__.pyc'. Начиная с 3.2 __file__ всегда указывает на исходный .py файл '/home/andrew/projects/py3k/Lib/xml/dom/__init__.py'. Встроенные модули не содержат этот атрибут. Для C Extensions __file__ указывает на имя .so или .pyd файла - '/usr/lib/python2.6/dist-packages/libvirtmod.so'. Впрочем, некоторые библиотеки этим пренебрегают и ведут себя как встроенные - жизнь несовершенна.
  • __path__: [str] - список файловых путей, в которых находится пакет. Существует только для пакетов. Об этом атрибуте я расскажу чуть позже более подробно.
  • __cached__: str - [3.2+] нововведение, появившееся в Python 3.2. Путь к .pyc файлу. Читайте дальше.
  • __package__: str - [2.5+] имя пакета, в котором лежит модуль (пустая строка для модулей верхнего уровня). Появился для поддержки относительного импорта from . import a. Текущая реализация содержит довольно серьезный баг: атрибут остается установленным в None в случаях абсолютного импорта по полному пути. PEP 328 - Imports: Multi-Line and Absolute/Relative.
  • __loader__: Loader - [2.3+] ссылка на объект, который выполнял загрузку данного модуля. Присутствует только для тех модулей, которые были обработаны через механизм расширения импорта. Будет рассмотрен в дальнейших статьях. Можете про него забыть на какое-то время.

Первые три атрибута тривиальны. На оставшиеся стоит обратить внимание.

Пакеты

Поддержка пакетов и питоне довольно интересная. Рассмотрим нетривиальный пример.

Допустим, мы должны поддерживать разные операционные системы.

При этом у нас есть код (скажем, ctypes вызовы) которые имеют смысл только для Windows, а в linux нужно писать совсем иначе.

Классический подход громоздит много if и elif (думаю именно потому, что авторы не всегда владеют Питоном в полной мере).

Существует и более элегантное решение:

  • package
    • __init__.py
    • a.py
    • linux2
      • b.py
    • win32
      • b.py

Мы кладем "общий" код непосредственно в package, а платформозависимый разносим по вложенным (технически они могут находится где угодно) папкам. Обратите внимание - linux2 и win32 не содержат __init.py__ и не являются вложенными пакетами.

А в __init__.py пишем что-то вроде:

import sys
from os.path import join, dirname
__path__.append(join(dirname(__file__), sys.platform))

В результате package.__path__ будет выглядеть как ['.../package', '.../package/linux2'], а windows specific модули в него не попадут и не смогут импортироваться при всем желании. Модульный полиморфизм в действии, долой if/else - частый признак плохого дизайна.

Просто делайте

from package import b

и получите то версию, работающую у вас. Другие - не сможете увидеть.

Кеширование модулей

Загрузка модуля из исходного .py файла - довольно накладное занятие. Нужно построить по питоновскому тексту исполняемый code block, создать модуль и выполнить код в его пространстве имен. Синтаксический анализ трудоемок, поэтому CPython использует кеширование. Когда программа просит загрузить модуль a, то питон смотрит на файлы a.py и a.pyc. Последний содержит уже скомпилированный код.

Если a.pyc существует и он не старее (смотрим дату модификации файлов) чем a.py - радостно используем этот кеш. Иначе делаем все медленно и печально, заодно создавая или перезаписывая a.pyc для последующего использования.

Теперь о том, что поменялось.

С самых бородатых времен оба файла лежали рядом. Это создавало определенные неудобства. Дело в том, что на компьютере можно установить несколько разных версий Питона, а .pyc файл один. Каждая версия добавляет/изменяет свой байткод, поэтому .pyc файлы оказываются несовместимыми.

Решают этот вопрос по разному. Обычно новые модули устанавливают в pythonx.x/site-packages. Для другой версии питона будет другой путь, и никто не подерется.

Все хорошо, но стороннюю библиотеку придется установить несколько раз - для каждой версии Питона заново.

Как по мне - это лучший выход, но ребята из Debian/Ubuntu пошли своим путем. Описывать его не буду, кто знает - поймет. Остальным это не нужно. Скажу лишь, что их способ взрывает мозг.

К счастью, вышел новый PEP 3147 - PYC Repository Directories, который реализован в Python 3.2.

Теперь все будет выглядеть так:

  • package
    • __init__.py
    • a.py
    • __pycache__

      • __init__.cpython-32.pyc
      • a.cpython-32.pyc

Внутри каждого пакета появляется папка __pycache__ для складывания в нее .pyc файлов.

Эту папку не прячут, начиная имя с точки и не устанавливают hidden attribute.

Кеш-файлы имеют имена вида <name>.<tag>.pyc. С именем все ясно. tag закодирован так, чтобы содержать версию питона - cpython-32: CPython версии 3.2. Оставлено место под другие разновидности Питона. Например, Unladen Swallow имеет несовместимый байткод - поэтому он сможет использовать свой уникальный префикс.

>>> dom.__cached__
'/home/andrew/projects/py3k/Lib/xml/dom/__pycache__/__init__.cpython-32.pyc'

Еще раз повторюсь - с выходом 3.2 __file__ указывает на .py файл, а не на .pyc.

Ремарка для педантов - читайте PEP 3174 для включения только .pyc в дистрибутив. Не люблю...

Резюме

Вот и все. Мы подробно прошлись по атрибутам модуля - какие они были и есть, что добавилось.

В следующей части я расскажу, где Питон находит модули и пакеты.

Если думаете, что все исчерпывается sys.path - вы глубоко заблуждаетесь.