Ликбез по пакетам и шпаргалка по модулям в Python
Как вы, возможно знаете, код на Python хранится в модулях (modules), которые могут быть объединены в пакеты (packages). Это руководство призвано подробно рассказать именно о пакетах, однако совсем не упомянуть модули нельзя, поэтому я немного расскажу и о них. Многое из того, что применимо к модулям, справедливо и для пакетов, особенно если принять во внимание тот факт, что каждый, как правило, ведёт себя как модуль.
Кратко о модулях
Модуль в Python — это файл с кодом. Во время же исполнения модуль представлен соответствующим объектом, атрибутами которого являются:
- Объявления, присутствующие в файле.
- Объекты, импортированные в этот модуль откуда-либо.
При этом определения и импортированные сущности ничем друг от друга не отличаются: и то, и другое — это всего лишь именованные ссылки на некоторые объекты первого класса (такие, которые могут быть переданы из одного участка кода в другой как обычные значения).
Такое единообразие удобно, например, при рефакторинге: мы можем разделить один разросшийся модуль на несколько, а потом импортировать вынесенные определения в оригинальный модуль. При этом с точки зрения внешнего наблюдателя переработанный модуль будет иметь те же атрибуты, которые имел до внесения изменений, а значит у пользователей модуля ничего в коде не сломается.
Модули и видимость содержимого
В Python нет настоящего сокрытия атрибутов объектов, поэтому и атрибуты объекта модуля так или иначе всегда доступны после импорта последнего. Однако существует ряд соглашений, которые влияют на процесс импортирования и поведение инструментов, работающих с кодом.
Так атрибуты, имя которых начинается с одиночного подчёркивания, считаются как бы помеченными "для внутреннего использования", и обычно не отображаются в IDE при обращению к объекту "через точку". И linter обычно предупреждает об использовании таких атрибутов, мол, "небезопасно!". "Опасность" состоит в том, что автор кода имеет полное право изменять состав таких атрибутов без уведомления пользователей кода. Поэтому программист, использовавший в своём коде приватные части чужого кода рискует в какой-то момент получить код, который перестанет работать при обновлении сторонней библиотеки.
Итак, мы можем определять публичные атрибуты модуля, приватные атрибуты (так называют упомянутые выше атрибуты "для внутреннего пользования"). И данное разделение касается не только определений, содержащихся в самом модуле, но и импортируемых сущностей. Ведь все импортированные объекты становятся атрибутами и того модуля, в который они импортированы.
Есть и третья группа атрибутов — атрибуты, добавляемые в область видимости при импортировании всего содержимого модуля ("со звёздочкой", from module import * ). Если ничего явно не указывать, то при таком импортировании в текущую область видимости добавятся все публичные атрибуты модуля. Помимо данного умолчания существует и возможность явно указать, что конкретно будет экспортировано при импорте со звёздочкой. Для управления названным методом импорта существует атрибут __all__ , в который можно положить список (а ещё лучше — кортеж) строк с именами, которые будут экспортироваться.
Живой пример видимости атрибутов модулей.
Рассмотрим пример, демонстрирующий всё вышеописанное. Пусть у нас будет два файла:
Рассмотрим сначала обычный импорт import module . Если импортировать модуль таким образом, то IDE, REPL и остальные инструменты "увидят" у модуля следующие атрибуты:
- FISH , MEAT т.к. имена констант — публичные,
- CAT , т.к. константа импортирована под публичным именем.
А эти атрибуты не будут видны:
- _DOG , т.к. при импортировании константа переименована в приватной манере,
- _GOAT , т.к. импортирована по своему приватному имени (тут линтер может и поругать за обращение к приватному атрибуту модуля!),
- _CARROT , ибо приватная константа.
Импорт import other_module я не рассматриваю как тривиальный случай.
Теперь рассмотрим импорт всего содержимого module:
После импортирования в текущей области видимости мы получим ровно два новых имени: FISH и _CARROT — именно они перечислены в атрибуте __all__ . Заметьте, что в данном случае при массовом импорте добавится даже приватный атрибут, потому что он явно указан!
Последствия импорта from other_module import * тоже очевидны и я их не рассматриваю.
Наконец-то, пакеты!
Пакет в Python — директория с обязательным модулем __init__.py . Остальное содержимое опционально и может включать в себя и модули, и другие пакеты.
Импортирование пакетов
Пакет с единственным модулем __init__.py при импорте ведёт себя как обычный модуль. Содержимое инициализирующего модуля определяет атрибуты объекта пакета.
Прочие модули пакета и вложенные пакеты не импортируются автоматически вместе с пакетом-родителем, но могут быть импортированы отдельно с указанием полного имени. Важный момент: при импортировании вложенного модуля всегда сначала импортируются модули инициализации всех родительских пакетов (если оные ещё ни разу не импортировались, но об этом я расскажу ниже).
Рассмотрим, к примеру, следующую структуру директорий и файлов:
Когда мы импортируем модуль submodule.py , то фактически происходит следующее (именно в таком порядке):
- загружается и выполняется модуль package/__init__.py ,
- загружается и выполняется package/subpackage/__init__.py ,
- наконец, импортируется package/subpackage/submodule.py .
При импорте package.module предварительно загружается только package/__init__.py .
Так что же, если мы загрузим парочку вложенных модулей, то для каждого будет выполняться загрузка всех __init__.py по дороге? Не будет! Подсистема интерпретатора, отвечающая за загрузку модулей, кэширует уже загруженные пакеты и модули. Каждый конкретный модуль загружается ровно один раз, в том числе и инициализирующие модули __init__.py (короткие имена модулей хоть и одинаковы, но полные имена всегда разные). Все последующие импортирования модуля не приводят к его загрузке, только лишь нужные атрибуты копируются в соответствующие области видимости.
Пакеты и __all__
В целом атрибут __all__ в модуле инициализации пакета ведёт себя так же, как и в случае с обычным модулем. Но если при импорте пакета "со звёздочкой" среди перечисленных имён встретится имя вложенного модуля, а сам модуль не окажется импортирован ранее в этом же __init__.py , то этот модуль импортируется неявно! Очередной пример это продемонстрирует.
Вот структура пакета:
Файл же package/__init__.py содержит следующее (и только это!):
А импортируем мы from package import * . В области видимости у нас окажутся объекты модулей a и b под своими именами (без полного пути, то есть без package. ). При этом сами модули в коде нигде явно не импортируются! Такая вот "автомагия".
Указанный автоматизм достаточно ограничен: не работает "вглубь", например — не импортирует "через звёздочку" указанные модули и подпакеты. Если же вам вдруг такого захочется, вы всегда сможете на соответствующих уровнях в __init__.py сделать from x import * и получить в корневом пакете плоскую область видимости со всем нужным содержимым. Но такое нужно довольно редко, потому что "не помогает" ни IDE, ни ручному поиску по коду. Впрочем, знать о фиче и иметь её в виду — не вредно, как мне кажется.
Изучайте Python на Хекслете Первые курсы в профессии Python-программист доступны бесплатно сразу после регистрации. Начните сегодня, учитесь в комфортном для вас темпе.
Пакеты, модули и точки входа
С модулем __init__.py разобрались. Настала очередь модуля __main__.py . Этот модуль позволяет сделать пакет исполняемым посредством вызова python -m … . Те из вас, кому знакомо оформление точки входа в модулях, могут догадаться, откуда ноги растут у магического выражения __name__ == '__main__' — да, отсюда! Для остальных напоминаю: чтобы модуль сделать "исполняемым, но не при импорте", в конец модуля дописывается конструкция
У модуля, который скармливается интерпретатору напрямую ( python file.py ) или в роли претендента на запуск ( python -m module ), атрибут __name__ будет содержать то самое магическое '__main__' . А в остальное время атрибут содержит полное имя модуля. С помощью условия, показанного выше, модуль может решить, что делать при запуске.
У пакетов роль атрибута выполняет специальный файл __main__.py . Когда мы запустим пакет через python path/to/package или python -m package , интерпретатор будет искать и выполнять именно этот файл.
Более того, модули __main__ нельзя импортировать обычным способом, поэтому можно не бояться случайного импорта и писать команды прямо на верхнем уровне: всё равно странно в модуле с именем __main__ проверять, что его имя равно __main__ (хе-хе!).
А ещё модуль __main__.py удобен тем, что его можно класть в корень вашего проекта, после чего запускать проект можно будет с помощью команды python . ! Лаконично, не правда ли?
PEP 420, или неявные пространства имён
Раз уж развёл ликбез, расскажу и про эту штуку.
Долгое время в Python пакеты были обязаны иметь файл __init__.py — наличие этого файла позволяло отличить пакет от обычной директории с модулями (с которыми Python работать не мог). Но с версии Python3.3 вступил в силу PEP 420, позволяющий создавать пространства имён "на вырост".
Теперь вы можете создавать пакет без __init__.py , и такой пакет сможет существовать полноценно, разве что при импорте содержимого не будет производиться инициализация. Но, конечно же, данное изменение делалось не с целью сэкономить на файлах. Подобные пакеты могут встречаться в путях поиска пакетов (о поиске пакетов я ниже расскажу) более одного раза: все встреченные структуры с общим корневым именем при загрузке схлопнутся в одно пространство имён.
Тут стоит отметить, что с полноценными пакетами подобное не срабатывало ранее и не будет работать в будущем. Если среди путей пакет с модулем инициализации находится в первый раз, все последующие пакеты с тем же именем будут проигнорированы. Это защищает вас от смешивания сторонних пакетов с системными. И даже просто от ошибок именования: назвав пакет так же, как называется встроенный пакет или модуль, вы получите ошибку — ваши определения не будут импортироваться.
Пакеты — пространства имён (Namespace Packages, NP) — а именно так называются пакеты без инициализации — не могут объединяться с полноценными пакетами, поэтому добавить что-то в системный пакет вам также не удастся. И тут всё защищено!
Какая же польза от неявных пространств имён? А вы представьте себя авторами, скажем, игрового движка. Вы хотите весь код держать в общем пространстве имён engine , но при этом не желаете, чтобы весь код поставлялся одним дистрибутивом (не каждому же пользователю нужны все-все компоненты движка). С NP вы можете в нескольких дистрибутивах использовать общее корневое имя engine , но разные подпакеты и подмодули. А на выходе вы получите возможность делать импорты вида
Важно: помните, если встретятся обычный пакет и NP с одинаковым именем, то победит обычный пакет! А NP, сколько бы их не было, не будут загружены!
Циклические импорты
Если вдруг вы захотите в один модуль импортировать другой, а другой захочет, пусть даже и не напрямую, импортировать первый, то вы получите ImportError . Потому что у вас случится циклический импорт. Про оный нужно просто знать и стараться архитектурить код так, чтобы циклов не случалось.
Если же приспичивает, и импортировать что-то "ну очень нужно", то можно попробовать обойтись локальным импортом:
Да, это костыль. Но иногда полезный. В идеале — до ближайшего большого рефакторинга. Поэтому настраивайте linter на ловлю локальных импортов и стремитесь убирать такие костыли хоть когда-нибудь!
Поиск пакетов и модулей
Пайтон ищет модули и пакеты в директориях, во время исполнения перечисленных в списке sys.path — по порядку от первого пути к последнему.
В этом списке пути до стандартных библиотек обычно расположены раньше, чем директории со сторонними пакетами, чтобы нельзя было случайно заменить стандартный пакет сторонним (помним: кто первый, того и тапки — среди нескольких с одинаковыми именами загружается первый попавшийся пакет).
В списке путей (обычно в начале) присутствует и путь '' , означающий текущую директорию. Это, в свою очередь, означает, что модули и пакет в текущем проекте имеют больший приоритет.
Обычно пути трогать не нужно, всё вполне нормально "работает само". Но если очень хочется, то путей у вас несколько:
- Использовать переменную окружения PYTHONPATH (значение — строка с путями, разделёнными символом : ),
- Во время исполнения изменить sys.path .
Первый способ — простой и понятный. Не сложнее добавления пути до исполняемых файлов в PATH (даже синтаксис тот же).
Второй способ — сложный и требующий внимательности. Дело в том, что sys.path нужно изменять максимально рано — где-нибудь в точке входа. Если не торопиться менять sys.path , то что-то уже может успеть загрузиться до того, как вы перестроите пути для поиска пакетов. А ведь эта загрузка может произойти в другом потоке исполнения! Отлаживать проблемы с очерёдностью загрузки модулей сложно. Лучше просто их не создавать.
Кстати, когда вы используете виртуальные окружения, sys.path будет содержать пути до локальных копий стандартных библиотек. Именно это позволяет виртуальному окружению быть самодостаточным (работать на любой машине с подходящей ОС — даже без установленного в систему Python!).
Что не было раскрыто?
Я специально не стал рассказывать про
- создание модулей и пакетов на лету (без использования файлов исходников);
- загрузку модулей не с диска, а из других источников;
- расширение подсистемы импортирования с целью загрузки в виде объектов-модулей чего-то, не являющегося кодом вовсе (XML, CSV, JSON).
Темы эти насколько интересны, настолько и велики. На наше счастье, самим разбираться в такой тонкой и сложной машинерии приходится редко. Мы просто пользуемся готовыми магическими артефактами, а зачаровывают их другие 🙂 Если же вы захотите научиться магии, документация вам в руки.
Python модули и пакеты
При написании объёмного кода, часто прибегают к разбиению такового на логически независимые блоки и к последующему выносу в другие файлы. Это повышает читаемость как самого кода, так и проекта целиком. Что влечет за собой менее ресурсозатратную поддержку(дальнейшую модификацию кодовой базы для разных нужд).
После разделения кода по файлам, следует выстроить их взаимодействие. В языке программирования Python данный механизм реализуется с использованием import. Импортировать можно любые компоненты(если Вы кодом не ограничивали таковые) модулей или пакетов.
Модули также могут быть написаны и на других языках, но данная статья направлена на рассмотрение использования данного механизмах в рамках Python.
Модули
В языке программирования Python модулями являются все файлы с расширением *.py (* обозначает, что на этом месте может стоять любой символ или любое их количество). Исключением является служебный файл __init__.py (о назначении которого описано далее в статье).
Дальше стоит понимать, что любая программа имеет некую точку входа. Это своего рода место с которого стартует наш скрипт. В языках предшественниках данной точкой служила функция main и могла быть лишь только одной. В нашем случае допускается отсутствие таковой, но это снижает качество кода, делая его сложным и малопредсказуемым(при импорте код содержащийся на верхнем уровне исполняется). Для того чтобы указать точку входа(может быть указана только в модулях) используется специальная переменная __name__ , в которой содержится наименование текущего модуля или пакета. Если текущий модуль находится на верхнем уровне исполнения(мы явно его передали на исполнение Python), то он называется __main__ независимо от названия файла.
Для примера реализуем простой модуль, который будет возвращать нам информацию:
Далее в корне создадим main.py файл, в который импортируем наш модуль двумя разными способами(об импортах описано в статье):
Все без проблем исполняется.
Трудности
При переносе файлов куда-либо из директории возникнут проблемы из-за первого импорта( main.py ). Поскольку часто приходится писать один и тот же код, использование уже хорошо написанного пакета или модуля может экономить много времени, но исправление большого количества импортов требует Ваших ресурсов. Хорошо написанный пакет, модуль или импорт может экономить ваши рабочие часы, а иногда и нервы.
Не изменяя наши модули(импорты), при изменении положения файлов возникает ошибка импорта:
Пакеты
В языке программирования Python пакетами являются все директории(вне зависимости от наличия в них модулей), содержащие файл __init__.py , который исполняется при импорте пакета и несет его название ( __name__ ).
Для примера реализуем простой пакет( package ), на базе вышеописанного модуля( http_get.py ):
А также реализуем простой пакет с такой же логикой, но с использованием абсолютного импорта:
В корне директории(на уровень выше пакета) создадим файл, в котором воспользуемся нашими пакетами( main.py ):
Все работает без ошибок.
Трудности
Но при переносе нашего package_2 в другой проект, он теряет свою работоспособность из-за ошибки импортирования в __init__.py файле, в отличии от package .
Данная статья написана для новичков, которые изучают язык программирования Python. Задача которой продемонстрировать на простых примерах способы написания пакетов и модулей(не является туториалом), а так же показать какие трудности могут возникнуть и пути их решения.
Github с проектом к данной статье: ModulesAndPackages
Может быть полезно выгрузить модуль или пакет и попробовать внедрить его в свой проект.
Пакеты в Python
Пакет — это каталог (папка), который может включать другие каталоги или модули. Модуль — это файл с исходным кодом, имеющий расширение .py. Пакеты используются для формирования пространства имен, что позволяет работать с модулями через указание уровня вложенности (с помощью оператора . ). Для импортирования пакетов используется тот же синтаксис, что и для работы с модулями.
Предположим, мы разрабатываем игру. Одна из возможных организаций наших пакетов и модулей может быть следующей:
Примечание: Каталог должен содержать файл с именем __init__.py , чтобы Python мог рассматривать этот каталог как пакет. Данный файл можно оставить пустым, но обычно туда помещают код инициализации этого пакета.
Импорт модуля из пакета в Python
В Python мы можем импортировать модули из пакетов, используя точку . . Например, если мы хотим импортировать модуль start из вышеприведенного пакета game, нам нужно написать следующее:
Теперь, если этот модуль содержит функцию с именем select_difficulty(), то для ее использования следует указать полную ссылку:
from … import
Если вышеприведенная ссылка на функцию кажется длинной, то мы можем импортировать модуль следующим образом:
9.1. Теория¶
Модули и пакеты являются неотъемлемой частью модульного программирования — организации программы как совокупности небольших независимых блоков, структура и поведение которых подчиняются определенным правилам.
Разработка программы как совокупности модулей позволяет:
упростить задачи проектирования программы и распределения процесса разработки между группами разработчиков;
предоставить возможность обновления (замены) модуля, без необходимости изменения остальной системы;
упростить тестирование программы;
упростить обнаружение ошибок.
Программный код часто разбивается на несколько файлов, каждый из которых используется отдельно от остальных. Одним из методов написания модульных программ является объектно-ориентированное программирование .
9.1.1. Основные понятия¶
Модуль (англ. Module) — специальное средство языка программирования, позволяющее объединить вместе данные и функции и использовать их как одну функционально-законченную единицу (например, математический модуль, содержащий тригонометрические и прочие функции, константы \(\pi\) , \(\epsilon\) и т.д.).
Пакеты (англ. Package) являются еще более крупной единицей и представляют собой набор взаимосвязанных модулей, предназначенных для решения задач определенного класса некоторой предметной области (например, пакет для решения систем уравнений, который может включать математический модуль, модуль со специальными типами данных и т.д.).
9.1.2. Модули и пакеты в Python¶
Модуль — отдельный файл с кодом на Python, содержащий функции и данные:
имеет расширение *.py (имя файла является именем модуля);
может быть импортирован (подключен) (директива import . );
может быть многократно использован.
Пакеты в Python — это способ структуризации модулей. Пакет представляет собой папку, в которой содержатся модули и другие пакеты и обязательный файл __init.py__ , отвечающий за инициализацию пакета.
Так, например, пакет xml имеет следующую структуру:
где каждый модуль (или вложенный пакет) отвечает за свою часть реализации работы с XML-форматом, однако рассматривается как единое целое в виде пакета.
Одна из основных целей использования как модулей, так и пакетов — реализация модели пространства имен, позволяющей логически группировать и в то же время изолировать различные идентификаторы. Например, при наличии глобальной переменной author в модуле A и B не произойдет конфликта, т.к. они находятся в разном пространстве имен: A.author и B.author соответственно.
9.1.2.1. Классификация¶
Все модули/пакеты в Python можно разделить на 4 категории:
Встроенные (англ. Built-in).
Модули, встроенные в язык и предоставляющие базовые возможности языка (написаны на языке Си).
К встроенным относятся как модули общего назначения (например, math или random ), так и плаиформозависимые модули (например, модуль winreg , предназначенный для работы с реестром ОС Windows, устанавливается только на соответствующей ОС).
Список установленных встроенных модулей можно посмотреть следующим образом:
Стандартная библиотека (англ. Standard Library).
Модули и пакеты, написанные на Python, предоставляющие расширенные возможности, например, json или os .
Сторонние (англ. 3rd Party).
Модули и пакеты, которые не входят в дистрибутив Python, и могут быть установлены из каталога пакетов Python (англ. PyPI — the Python Package Index, более 90.000 пакетов) с помощью утилиты pip :
При установке пакета автоматически устанавливаются зависимые пакеты.
Модули и пакеты, создаваемые разработчиком.
Примечание
Создание собственных пакетов не рассматривается в рамках настоящего курса.
В собственной программе рекомендуется выполнять импорт именно в таком порядке: от встроенных до собственных модулей/пакетов.
9.1.2.2. Подключение и использование¶
Для использования модуля или пакета в коде необходимо его предварительно подключить (импортировать).
Импорт модуля или пакета выполняется единожды инструкцией import , располагаемой, как правило, в начале файла.
Выполнить подключение модуля можно несколькими способами:
Пакет подключается аналогичным образом. Кроме того, имеется возможность импорта отдельных модулей из пакета, если нет необходимости использовать весь пакет:
Различные варианты подключения модуля и пакета приведены в Листингах 9.1.1 (а-г).