Как python знает, какие пакеты импортировать, где их найти и как современные инструменты (conda, pyenv, поэзия) упрощают нам это

Ранее мы рассмотрели (статья здесь), как различные инструменты (conda, pyenv) манипулируют переменной $PATH так, что когда вы вводите python, версия python, которую вы хотите (в данной среде), является версией python, которую вы получаете . Это все отлично подходит для импорта стандартной библиотеки функций Python по умолчанию, но как насчет:

  • когда мы хотим установить сторонние пакеты из PyPI (например, numpy, pandas, scipy)
  • когда мы хотим локально установить наши собственные пакеты/модули — либо материал для разработки, либо тестирование перед сборкой собственных пакетов для PyPI.

Откуда питон знает:

  • где искать эти пакеты
  • как расставить приоритеты, если у нас есть два с одинаковым именем

Для этого он создает «путь поиска модуля» в ту секунду, когда вы вводите python.

Что происходит, когда вы вводите python?

Итак, мы уже знаем, что когда вы вводите python, ваша операционная система просматривает сверху вниз путь к первому исполняемому файлу с именем «python» (независимо от того, является ли он на самом деле исполняемым файлом python, как указали прокладки pyenv). Затем, как только он находит его, он выполняет его. Но что это делает? Что будет дальше? В контексте знания того, как найти наши пакеты, нам важно, как он строит переменную sys.path.

Подобно $PATH, sys.path является внутренней версией Python и подобно тому, как операционная система проходит сверху вниз $PATH для сопоставления исполняемых файлов, python проходит sys.path для сопоставления пакетов и модулей.



Python, системный путь и как conda и pyenv манипулируют им
Глубокое погружение в то, что происходит, когда вы вводите «python
в оболочке, и как популярные инструменты управления средой манипулируют… навстречу datascience.com»



Как определяется и устанавливается sys.path?

Есть отличная статья Здесь, в которой рассказывается о тонкостях этого и о том, что, хотя это кажется простой задачей, есть много препятствий, через которые нужно пройти. С современными настройками Python, где нам посчастливилось использовать один или несколько из них:

  • пиенв
  • конда
  • поэзия

этот процесс в значительной степени упрощен, так как мы на самом деле не слишком заботимся о таких вещах, как $PYTHONPATH и $PYTHONHOME, поскольку они, как правило, остаются пустыми.

Давайте рассмотрим пример. Я использую pyenv и точно так же, как прежде, чем мы сможем проверить, какую версию Python мы используем:

> pyenv version
3.9.7 (set by /Users/jamisonm/.pyenv/version)

Учитывая, что мы рассмотрели в предыдущей статье здесь, как на самом деле работает pyenv для поиска и установки исполняемого файла, мы знаем, что в этом случае мы используем /Users/jamisonm/.pyenv/versions/3.9.7/bin/python в качестве исполняемого файла python. Так что же происходит сейчас?

Найти местоположение этой версии Python, которая выполняется

Таким образом, операционная система выполняет исполняемый файл python, а затем исполняемая программа запрашивает у операционной системы свое местоположение. При условии отсутствия сбоев это затем устанавливается в переменной sys.executable=/Users/jamisonm/.pyenv/versions/3.9.7/bin/python.

Установите sys.prefix и sys.exec_prefix

Затем python устанавливает префикс и префикс exec_prefix, которые обычно совпадают. Если наша переменная sys.executable установлена ​​на /Users/jamisonm/.pyenv/versions/3.9.7/bin/python, то sys.prefix=sys.exec_prefix=/Users/jamisonm/.pyenv/versions/3.9.7/. Однако, если файл pyvenv.cfg существует в каталоге выше sys.executable, то он читается, и оба sys.prefix и sys.exec_prefix устанавливаются в этот каталог - это контролируется этой функцией здесь.

Установите sys.path, используя sys.prefix и sys.exec_prefix

Теперь Python импортирует модуль сайт и выполняет его основную функцию. Согласно документам здесь, это делает следующее:

«Все начинается с создания до четырех каталогов из головной и конечной частей. Для головной части используются sys.prefix и sys.exec_prefix; пустые головы пропускаются. В хвостовой части используется пустая строка, а затем lib/site-packages (в Windows) или lib/pythonX.Y/site-packages (в Unix и macOS). Для каждой из различных комбинаций "голова-конец" он определяет, ссылается ли он на существующий каталог, и если да, то добавляет его в sys.path, а также проверяет вновь добавленный путь на наличие файлов конфигурации".

На практике это добавляет следующее к пути sys:

/Users/jamisonm/.pyenv/versions/3.9.7/lib/python39.zip
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/lib-dynload
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/site-packages

Вот и все. Теперь у нас есть переменная sys.path. Как и в случае с $PATH и операционной системой, python просматривает переменную sys.path сверху вниз в поисках совпадающих имен модуля и пакета, и когда он находит одно, он выполняет этот фрагмент кода Python. Единственная разница с $PATH заключается в том, что python сначала проверяет локальный каталог вызовов на наличие соответствующих модулей, прежде чем проходить через sys.path (подробнее об этом ниже).

Что такое «сайт-пакеты»?

Сюда попадают все сторонние пакеты, которые вы загружаете из PyPI (через pip, conda, поэзию и т. д.). Когда мы видим вещи таким образом, идея управления средой и обеспечения того, чтобы все зависимости находились в нужном месте, становится довольно тривиальной. Как только у нас будет правильное расположение исполняемого файла python, мы просто убедимся, что соблюдаем правила, и поместим его в соответствующую папку site-packages. Когда мы вводим python в этой среде, тогда:

  • операционная система проходит через переменную $PATH, чтобы найти и выполнить правильную версию python
  • затем операционная система передает это местоположение исполняемому файлу python, поскольку он инициализирует такие вещи, как sys.executable
  • python также создает переменную sys.path на основе приведенной выше формулы, указывая, что пакеты нужно искать в соответствующей папке site-packages.

Как насчет установки пакета локально?

Вышеприведенное прекрасно работает, если мы хотим настроить среду с определенной версией Python и связанными зависимостями, которые мы можем получить из PyPI. Но как быть с тем, чего нет в PyPI? Или что, если мы вытащим пакет из PyPI и изменим его (возможно, для улучшения) и захотим его использовать?

Для этого у python есть другое решение — файлы .pth. Из документации по python здесь:

«Файл конфигурации пути — это файл, имя которого имеет вид name.pth и находится в одном из четырех каталогов, упомянутых выше; его содержимое — это дополнительные элементы (по одному на строку), которые необходимо добавить в sys.path. Несуществующие элементы никогда не добавляются в sys.path, и не выполняется проверка того, что элемент относится к каталогу, а не к файлу. Ни один элемент не добавляется в sys.path более одного раза. Пустые строки и строки, начинающиеся с #, пропускаются. Строки, начинающиеся с импорта (за которым следует пробел или табуляция), выполняются».

Итак, чтобы написать код локально и гарантировать, что при запуске Python где-нибудь мы сможем импортировать код, который нам нужно:

  • напишите код в файле с именем module_name.py
  • где-нибудь в одном из каталогов в sys.path (обычно site-packages) добавьте файл с именем module_name.pth, который содержит каталог, в котором находится наш модуль

Это именно то, что происходит, когда вы запускаете что-то вроде poetry install или pip install . -e — в папку site-packages добавляется файл .pth, который добавляет это местоположение к переменной sys.path.

Давайте проверим это с помощью тестового модуля. В новом каталоге создайте папку с именем test. В моем домашнем каталоге у нас есть mkdir ~/test. Затем мы можем добавить следующий файл в этот каталог с именем datetime.py (преднамеренно названный так, чтобы конфликтовать с файлом datetime.py в стандартной библиотеке Python. В общем, у нас есть следующее:

> mkdir ~/test
> cd test
> touch datetime.py

а затем добавьте следующее к datetime.py:

def hello_world():
    print('hello world')
    return

Наконец, нам нужно добавить наш файл test.pth в каталог site-packages нашей текущей среды. Как указано выше, с pyenv (в моих глобальных настройках с использованием Python версии 3.9.7) мой каталог находится в /Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/site-packages. Затем мы можем добавить к нему следующее:

> cd /Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/site-packages
> echo "/Users/jamisonm/test" > test.pth

где последняя команда — это одна строка для создания текстового файла с нужным нам каталогом. Теперь, если мы запустим python и запустим sys.path, мы увидим следующее:

>>> import sys
>>> print('\n'.join(sys.path))
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python39.zip
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/lib-dynload
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/site-packages
/Users/jamisonm/test

где была добавлена ​​нижняя запись. Это связано с тем, что когда python сканирует предыдущие 4 местоположения в sys.path, он нашел файл .pth (файл test.pth, который мы создали) и, соответственно, добавил его в sys.path в конце. Важно, что это означает:

  • python сначала будет искать в других местах совпадение с именем пакета или модуля, прежде чем искать в /Users/jamisonm/test
  • до этого python фактически ищет совпадение в текущем каталоге

Это означает, что если мы запустим из /Users/jamisonm/test следующее, мы увидим:

import datetime
datetime.date.today()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'datetime' has no attribute 'date'

потому что модуль datetime.py, который мы определили в /Users/jamisonm/test, имеет приоритет над остальной частью sys.path, включая модуль стандартной библиотеки python datetime.py, определенный в /Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9. Но если мы выйдем из каталога test и повторим, мы увидим:

import datetime
datetime.date.today()
datetime.date(2021, 11, 5)
import sys
print('\n'.join(sys.path))
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python39.zip
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/lib-dynload
/Users/jamisonm/.pyenv/versions/3.9.7/lib/python3.9/site-packages
/Users/jamisonm/test

то есть python сначала находит модуль стандартной библиотеки datetime.py (который содержит функцию datetime.date.today()), хотя /Users/jamisonm/test все еще находится в нашем sys.path.

Заключение

Подобно тому, как ваша операционная система определяет, какую версию Python запускать, когда вы вводите python, обходя сверху вниз вашу переменную $PATH, python создает и ищет путь поиска модуля (в основном, создавая переменную sys.path), чтобы найти установленные пакеты и который должен иметь приоритет, если у нас есть конфликт имен.

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

  • операционная система находит правильную версию Python для использования
  • используя эту информацию, python определяет sys.path для создания пути поиска модуля.
  • Файлы .pth можно добавить в одно из этих мест, чтобы добавить расположение локальных модулей в конец пути поиска модулей.

Такие инструменты, как conda и pyenv+poetry, делают именно это — они упрощают нам выполнение этих шагов, создавая, изменяя и управляя этими исполняемыми файлами python, соответствующими каталогами пакетов сайтов и любыми файлами .pth, добавленными к ним для облегчения локального разработка.