Вы когда-нибудь задумывались о том, как работают библиотеки виртуального окружения в Python? В этой статье я предлагаю ознакомится с главной концепцией, которую используют все библиотеки для окружений, такие как virtualenv, virtualenvwrapper, conda, pipenv.

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

Когда Python запускает интерпретатор, он начинает искать директорию с модулями (site-packages). Поиск начинается с родительской директории относительно физического расположения исполняемого файла интерпретатора (python.exe). Если папка с модулями не найдена, то Python переходит на уровень выше, и делает это до тех пор, пока не будет достигнута корневая директория. Для того, чтобы понять, что это директория с модулями, Python ищет модуль os, который должен лежать в файле os.py и является обязательным для работы питона.

Давайте представим, что наш интерпретатор располагается по адресу /usr/dev/lang/bin/python. Тогда пути поиска будут выглядеть так:

/usr/dev/lang/lib/python3.7/os.py
/usr/dev/lib/python3.7/os.py
/usr/lib/python3.7/os.py
/lib/python3.7/os.py

Как вы можете видеть, Python добавляет специальный префикс (lib/python$VERSION/os.py) к нашему пути. Как только интерпретатор находит первое совпадение (наличие файла os.py), он изменяет sys.prefix и sys.exec_prefix на этот путь (с удаленным префиксом). Если по каким-то причинам совпадений не найдено, то используется стандартный путь, который вкомпилирован в интерпретатор.

Теперь давайте посмотрим как это делает одна из самых старых и известных библиотек — virtualenv.

user@arb:/usr/home/test# virtualenv ENV
Running virtualenv with interpreter /usr/bin/python3
New python executable in /usr/home/test/ENV/bin/python3
Also creating executable in /usr/home/test/ENV/bin/python
Installing setuptools, pkg_resources, pip, wheel...done.

После выполнения, она создает дополнительные директории:

user@arb:/usr/home/test/ENV# tree -L 3
.
+-- bin
¦   +-- activate
¦   +-- activate.csh
¦   +-- activate.fish
¦   +-- activate_this.py
¦   +-- easy_install
¦   +-- easy_install-3.7
¦   +-- pip
¦   +-- pip3
¦   +-- pip3.7
¦   +-- python
¦   +-- python-config
¦   +-- python3 -> python
¦   +-- python3.7 -> python
¦   L-- wheel
+-- include
¦   L-- python3.7m -> /usr/include/python3.7m
+-- lib
¦   L-- python3.7
¦   +-- __future__.py -> /usr/lib/python3.7/__future__.py
¦   +-- __pycache__
¦   +-- _bootlocale.py -> /usr/lib/python3.7/_bootlocale.py
¦   +-- _collections_abc.py -> /usr/lib/python3.7/_collections_abc.py
¦   +-- _dummy_thread.py -> /usr/lib/python3.7/_dummy_thread.py
¦   +-- _weakrefset.py -> /usr/lib/python3.7/_weakrefset.py
¦   +-- abc.py -> /usr/lib/python3.7/abc.py
¦   +-- base64.py -> /usr/lib/python3.7/base64.py
¦   +-- bisect.py -> /usr/lib/python3.7/bisect.py
¦   +-- codecs.py -> /usr/lib/python3.7/codecs.py
¦   +-- collections -> /usr/lib/python3.7/collections
¦   +-- config-3.7m-darwin -> /usr/lib/python3.7/config-3.7m-darwin
¦   +-- copy.py -> /usr/lib/python3.7/copy.py
¦   +-- copyreg.py -> /usr/lib/python3.7/copyreg.py
¦   +-- distutils
¦   +-- encodings -> /usr/lib/python3.7/encodings
¦   +-- enum.py -> /usr/lib/python3.7/enum.py
¦   +-- fnmatch.py -> /usr/lib/python3.7/fnmatch.py
¦   +-- functools.py -> /usr/lib/python3.7/functools.py
¦   +-- genericpath.py -> /usr/lib/python3.7/genericpath.py
¦   +-- hashlib.py -> /usr/lib/python3.7/hashlib.py
¦   +-- heapq.py -> /usr/lib/python3.7/heapq.py
¦   +-- hmac.py -> /usr/lib/python3.7/hmac.py
¦   +-- imp.py -> /usr/lib/python3.7/imp.py
¦   +-- importlib -> /usr/lib/python3.7/importlib
¦   +-- io.py -> /usr/lib/python3.7/io.py
¦   +-- keyword.py -> /usr/lib/python3.7/keyword.py
¦   +-- lib-dynload -> /usr/lib/python3.7/lib-dynload
¦   +-- linecache.py -> /usr/lib/python3.7/linecache.py
¦   +-- locale.py -> /usr/lib/python3.7/locale.py
¦   +-- no-global-site-packages.txt
¦   +-- ntpath.py -> /usr/lib/python3.7/ntpath.py
¦   +-- operator.py -> /usr/lib/python3.7/operator.py
¦   +-- orig-prefix.txt
¦   +-- os.py -> /usr/lib/python3.7/os.py
¦   +-- posixpath.py -> /usr/lib/python3.7/posixpath.py
¦   +-- random.py -> /usr/lib/python3.7/random.py
¦   +-- re.py -> /usr/lib/python3.7/re.py
¦   +-- readline.so -> /usr/lib/python3.7/lib-dynload/readline.cpython-37m-darwin.so
¦   +-- reprlib.py -> /usr/lib/python3.7/reprlib.py
¦   +-- rlcompleter.py -> /usr/lib/python3.7/rlcompleter.py
¦   +-- shutil.py -> /usr/lib/python3.7/shutil.py
¦   +-- site-packages
¦   +-- site.py
¦   +-- sre_compile.py -> /usr/lib/python3.7/sre_compile.py
¦   +-- sre_constants.py -> /usr/lib/python3.7/sre_constants.py
¦   +-- sre_parse.py -> /usr/lib/python3.7/sre_parse.py
¦   +-- stat.py -> /usr/lib/python3.7/stat.py
¦   +-- struct.py -> /usr/lib/python3.7/struct.py
¦   +-- tarfile.py -> /usr/lib/python3.7/tarfile.py
¦   +-- tempfile.py -> /usr/lib/python3.7/tempfile.py
¦   +-- token.py -> /usr/lib/python3.7/token.py
¦   +-- tokenize.py -> /usr/lib/python3.7/tokenize.py
¦   +-- types.py -> /usr/lib/python3.7/types.py
¦   +-- warnings.py -> /usr/lib/python3.7/warnings.py
¦   L-- weakref.py -> /usr/lib/python3.7/weakref.py
L-- pip-selfcheck.json

Как вы можете видеть, виртуальное окружение было создано путём копирования бинарника Python в локальную папку (ENV/bin/python). Так же мы можем заметить, что родительская папка содержит символические ссылки на файлы стандартной библиотеки питона. Мы не можем создать символическую ссылку на исполняемый файл, т.к. интерпретатор всё равно разименует её до фактического пути.

Теперь давайте активируем наше окружение:

user@arb:/usr/home/test# source ENV/bin/activate

Эта команда меняет переменную окружения $PATH, таким образом, чтобы команда python указывала на нашу локальную версию питона. Это достигается путём подстановки локального пути папки bin в начало строки $PATH, чтобы локальный путь имел приоритет перед всеми путями справа.

export "/usr/home/test/ENV/bin:$PATH"
echo $PATH

Если вы запустите скрипт из этого окружения, то он выполнится с помощью бинарника по адресу /usr/home/test/ENV/bin/python. Интерпретатор будет использовать этот путь как стартовую точку для поиска модулей. В нашем случае, модули стандартной библиотеки будут найдены по пути /usr/home/test/ENV/lib/python3.7/.

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

Улучшения в Python 3


Начиная с версии Python 3.3, появился новый стандарт, именуемый как PEP 405, который вводит новый механизм для легковесных окружений.

Этот PEP добавляется дополнительный шаг к процессу поиска. Если создать файл конфигурации pyenv.cfg, то вместо копирования бинарника Python и всех его модулей, можно просто указать их расположение в этом конфиге.

Эту фичи активно использует стандартный модуль venv, который появился в Python 3.

user@arb:/usr/home/test2# python3 -m venv ENV
user@arb:/usr/home/test2# tree -L 3
.
L-- ENV
  +-- bin
  ¦   +-- activate
  ¦   +-- activate.csh
  ¦   +-- activate.fish
  ¦   +-- easy_install
  ¦   +-- easy_install-3.7
  ¦   +-- pip
  ¦   +-- pip3
  ¦   +-- pip3.5
  ¦   +-- python -> python3
  ¦   L-- python3 -> /usr/bin/python3
  +-- include
  +-- lib
  ¦   L-- python3.7
  +-- lib64 -> lib
  +-- pyvenv.cfg
  L-- share
  L-- python-wheels

user@arb:/usr/home/test2# cat ENV/pyvenv.cfg
home = /usr/bin
include-system-site-packages = false
version = 3.7.0
user@arb:/usr/home/test2# readlink ENV/bin/python3
/usr/bin/python3

Благодаря этому конфигу, вместо копирования бинарника, venv просто создает ссылку на него. Если параметр include-system-site-packages изменить на true, то все модули стандартной библиотеки будут автоматически доступны из виртуального окружения.

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

P.S.: Я являюсь автором этой статьи, можете задавать любые вопросы.

Комментарии (2)


  1. hssergey
    31.07.2018 13:00

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


    1. rushter Автор
      31.07.2018 13:21
      +1

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