четверг, 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 - вы глубоко заблуждаетесь.