Иногда возникает необходимость разделить несколько пакетов, лежащих в одном пространстве имен по разным физическим путям. Например, если вы хотите иметь возможность передавать разную компоновку плагинов, имея возможность в последствии добавлять их, не контролируя их расположение, и, при этом, обращаться к ним через один namespace.
Эта шпаргалка, которая подойдет скорее для новичков, посвящена пространствам имен Python.
Давайте рассмотрим, как это можно сделать в разных версиях Python, так как хотя Python2 и перестает скоро поддерживаться, многие из нас как раз сейчас меж двух огней, и это как раз один из важных нюансов при переходе.
Рассмотрим такой пример:
Мы хотим получить структуру пакетов:
namespace1
package1
module1
package2
module2
Содержимое файла module1
print('package 1')
var1 = 1
Содержимое файла module2
print('package 2')
var2 = 2
При этом пакеты распределены в такой структуре папок:
path1
namespace1
package1
module1
path2
namespace1
package2
module2
Допустим, что так или иначе path1 и path2 уже добавлены в sys.path. Нам надо получить доступ к module1 и module2:
from namespace1.package1 import module1
from namespace1.package2 import module2
Что произойдет в Python 3.7 при выполнении этого кода? Все работает чудесно:
package 1
package 2
С PEP-420 в Python 3.3, появилась поддержка неявных пространств имен. Кроме того при импорте пакета с версии py33 не надо создавать файлы __init__.py. А при импорте namespace, это просто _запрещено_. Если в одной или обоих директориях и именем namespace1 будет присутствовать файл __init__.py, произойдет ошибка на импорте второго пакета.
ModuleNotFoundError: No module named 'namespace1.package2'
Таким образом наличие инишника явно определяет пакет, а пакеты объединять нельзя, это единая сущность. Если вы начинаете новый, независящий от старых разработок, проект и пакеты будут устанавливаться с помощью pip, то придерживаться надо именно такого способа. Однако иногда нам в наследство достается старый код, который тоже надо поддерживать, по-крайней мере некоторое время, или переносить на новую версию.
Перейдем к Python 2.7. С этой версией уже интереснее, нужно сначала добавлять __init__.py в каждую директорию для создания пакетов, иначе интерпретатор просто не распознает в этом наборе файлов пакет. А затем прописать в __init__ файлах относящихся к namespace1 явное объявление пространства имен, в противном случае, произойдет импорт только первого пакета.
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
Что при этом происходит? Когда интерпретатор доходит до первого импорта, выполняется поиск в sys.path пакета с таким именем, он находится в path1/namespace1 и интерпретатор выполняет path1/namespace1/__init__.py. Далее поиск не ведется. Однако функция extend_path сама выполняет поиск уже по всему sys.path, находит все пакеты с именем namespace1 и инишником и добавляет их в переменную __path__ пакета namespace1, которая используется для поиска дочерних пакетов в этом пространстве имен.
В официальных гайдах рекомендуется, чтобы инишники были одинаковыми при каждом размещении namespace1. На самом деле, они могут быть пустыми все, кроме первого, который находится при поиске в sys.path, в котором должен быть вызов pkgutil.extend_path, потому что остальные не выполняются. Однако, конечно, лучше чтобы действительно вызов был в каждом инишнике, чтобы не завязывать свою логику «на случай» и не гадать какой инишник выполнился первым, ведь порядок поиска может измениться. По этой же причине не стоит располагать никакую другую логику __init__ файлах области переменных.
Это сработает и в последующих версиях и этот код можно использовать для написания совместимого кода, но нужно учитывать, что выбранного способа надо придерживаться в каждом распространяемом пакете. Если на 3-й версии в некоторые пакеты положить инишник в вызовом pkgutil.extend_path, а некоторые оставить без инишника, это не сработает.
Кроме того этот вариант подходит и для случая, когда вы планируете устанавливать с помощью python setup.py install.
Еще один способ, который сейчас считается несколько устаревшим, но его еще можно много где встретить:
#namespace1/__init__.py
__import__('pkg_resources').declare_namespace(__name__)
Модуль pkg_resources поставляется с пакетом setuptools. Здесь смысл такой же, что и в pkgutil — надо, чтобы каждый __init__ файл при каждом размещении namespace1 содержал одинаковое объявление пространства имен и отсутствовал любой другой код. При этом в setup.py надо регистрировать пространство имен namespace_packages=['namespace1']. Более подробное описание создания пакетов выходит за пределы этой статьи.
Кроме того часто можно встретить, такой код
try:
__import__('pkg_resources').declare_namespace(__name__)
except:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
Здесь логика простая — если не установлен setuptools, то используем pkgutil, который входит в стандартную библиотеку.
Если настроить одним из этих способов пространство имен, то из одного модуля можно звать другой. Например, изменим namespace1/package2/module2
import namespace1.package1.module1
print(var1)
И далее посмотрим, что будет, если мы по ошибке назвали новый пакет так же как уже существующий и обернули тем же namespace'ом. Например, будут два пакета в разных местах с названием package1.
namespace1
package1
module1
package1
module2
В этом случае импортирован будет только первый и доступа к module2 не будет. Пакеты объединить нельзя.
from namespace1.package1 import module1
from namespace1.package1 import module2
#>>ImportError: cannot import name module2
Резюме:
- В случае Python старше 3.3 и установки с помощью pip рекомендуется использовать неявное объявление пространства имен.
- В случае поддержки версий 2 и 3, а так же установки и с pip и с python setup.py install, рекомендуется вариант с pkgutil.
- Вариант pkg_resources рекомендуется, если надо поддерживать старые пакеты, использующие такой метод, или вам надо чтобы пакет был zip-safe.
Источники:
- packaging.python.org/guides/packaging-namespace-packages
- www.python.org/dev/peps/pep-0420
- www.python.org/dev/peps/pep-0365 — не выполнен
- Learning Python, 5th Edition by Mark Lutz
Примеры можно посмотреть здесь.
Автор: elmos