Модули и пакеты

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

Модули позволяют нам:

  • Разбивать логику приложения
  • Повторно использовать написанный код
  • Выделять отдельное пространство имен

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

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

Модуль - файл проимпортируемый в главный файл или в другой модуль. Каждый модуль определяет свое пространство имен.

Импорт модулей

Для того чтобы проимпортировать модуль используются инструкции import и from/import. Прежде чем мы начнем импортировать модули давайте посмотрим на процесс импортирования модуля:

  1. Найти файл модуля.
  2. Скомпилировать файл в байт-код (если это необходимо).
  3. Запустить программный код модуля, чтобы создать объекты, которые он определяет.

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

Поиск модуля

Поиск модуля осуществляется в следующей последовательности:

  1. Домашний каталог программы.
  2. Содержимое переменной окружения PYTHONPATH (если таковая определена).
  3. Каталоги стандартной библиотеки.
  4. Содержимое любых файлов с расширением .pht (если таковые имеются).

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

import sys
print(sys.path)

В моем случае это выведет что-то такое:

['', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python27.zip', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-darwin', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac/lib-scriptpackages', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-old', '/usr/local/Cellar/python@2/2.7.17/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload', '/Users/vladimirzhdanov/Library/Python/2.7/lib/python/site-packages', '/usr/local/lib/python2.7/site-packages']

Поиск по этим папкам будет происходить последовательно, внутри папки будут искаться файлы следующих типов: .py, .pyc, .so/.dll, .zip

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

TODO

Импорт значений из модуля (from/import)

Зачастую вы не хотите импортировать модуль как объект, а вас интересует только некоторые переменные из модуля, в таком случае можно испльзовать конструкцию from <module> import var1, var2. При использовании этой конструкции вы копируете имено из области видимости одного модуля в область видимости другого.

from sys import path
print(path)

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

Использование as

Если же вы уже импортируете имя занятое в вашем модуле, вы можете сделать переименование импортируемого имени с помощью конструкции: from <module> import var1 as another_name

from sys import path
path = "/some/path" # Это глобальная переменная название которой уж сильно мне нравится и которой мы перезаписали значение переменной path из модуля sys
print(path) # выведется 'some/path'

Давайте исправим это:

from sys import path as sys_path
path = "/some/path"
print(path) # здесь все так же выводится '/some/path', но мы все еще имеем доступ к `sys.path` через переменную `sys_path`

Инструкция from *

⚠️ Предупреждаю

Использовать это инструкцию не рекомендуется

Мы с вами уже посмотрели на from/import, и поняли, что мы можем импортировать не только модуль целиком и обращаться к его именам через module.var но и сразу импортировать в текущее пространство имен нужные нам имена из модуля, мы обсудили и то, какие проблемы это может вызывать.

❗️ Для справки

В программировании очень часто используется * под значением "все"

Для того чтобы скопировать все имена из пространства модуля в текущий скоуп может использоваться конструкция from <module> import *, но это тянет за собой опасности.

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

Пакеты

До сих пор, импортируя модули, мы загружали файлы. Это типичный способ использования модулей и, скорее всего, этот прием будет вами использоваться наиболее часто в начале вашей карьеры программиста на языке Python. Однако возможности импортирования модулей немного богаче, чем я предлагал вам считать до настоящего момента. (Из Лутца)

Отлично! У нас с вами есть модули, но как строить иерархию папок, в вашей программе? В будущем мы часто будет ложить контроллеры в папку controllers, а какие-то дополнительные функции в директорию utils - это удобно. Такие директории мы будем называть "пакетами/пакетами модулей". Давайте несколько утверждений:

  • Пакет это директория
  • Пакет содержит модули
  • Пакеты могут составлять иерархию
  • Для того чтобы директория стала пакетом в ней должен находиться файл __init__.py

Процесс импортирования пакетов

Когда интерпретатор Python импортирует каталог в первый раз, он автоматически запускает программный код файла __init__.py этого каталога. По этой причине обычно в эти файлы помещается программный код, выпол- няющий действия по инициализации, необходимые для файлов в пакете. Например, этот файл инициализации в пакете может использоваться для создания файлов с данными, открытия соединения с базой данных и так далее. Обычно файлы __init__.py не предназначены для непосредственного выполнения – они запускаются автоматически, когда выполняется первое обращение к пакету. (Лутц)

Импортирование пакетов

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

├── app.py
├── controllers
│   ├── __init__.py
│   └── users_controller.py
└── utils
├── __init__.py
├── fs.py
└── images
├── __init__.py
├── croper.py
└── resizer.py

Для того чтобы проимпортировать модуль users_controller.py из app.py мы напишем следующее:

# app.py
from controllers import users_controller

❗️Вопрос

Что мы напишем для того чтобы проимпортировать модуль resizer ?

Относительный импорт модулей

Помните как находятся модули? Как говорилось модули сначала ищутся относительно "главного файла", но что делать если мы в пакете? В пакетах нам помогают относительные импорты, это когда путь до модуля мы описываем относительно текущего модуля... снова воспользуюсь иерархией представленной выше. Для того чтобы проимпортировать из resizer.py модуль croper.py мы воспользуемся относительным импортом:

# resizer.py
from . import croper# Можем импортировать модуль из текущего пакета
from .croper import crop_image # Можем импортировать имя из модуля

Абсолютный импорт

Помимо относительного импорта мы можем воспользоваться и абсолютным

# resizer.py
from utils.images.croper import crop_image

Переменная __all__

Хочу напомнить про синтаксис from *, для того чтобы ограничеть имена импортируемые таким образом мы можем добавить переменную __all__ в модуль или __init__.py и перечислить в ней те имена, которые могут импортироваться таким образом.

TODO: дописать