Глубокое погружение в то, что происходит, когда вы набираете «python», и как популярные инструменты манипулируют этим.

Системный путь (в Mac/Linux echo $PATH или echo -e ${PATH//:/\\n} для немного более красивой версии) — это не просто вещь Python, но очень важная для функционирования Python. Если вы открываете командную строку и вводите «команду», компьютер (или «операционная система») должен знать, где искать эту команду, чтобы найти базовый код для нее, а затем «выполнить» ее. Эти «командные файлы» широко известны как исполняемые файлы, и они запускаются, когда вы вводите слово, совпадающее с именем их файла.

Возьмем пример echo. Когда мы вводим echo $PATH, наш компьютер должен знать, где найти код команды echo и выполнить ее, передав аргумент - $PATH. Мы можем увидеть, где находится этот код, с помощью следующего:

> whereis echo
/bin/echo

где whereis — это еще один исполняемый файл, который наш компьютер знает, как найти:

> whereis whereis
/usr/bin/whereis

Интересный. Они сидят в двух разных местах. Что, если бы у нас было одно и то же имя исполняемого файла в обоих этих местах? Кого бы мы казнили?

Порядок системных путей

Переменная $PATH диктует это, и, к счастью, операционная система делает простую вещь и работает с ней слева направо / сверху вниз. В качестве примера давайте посмотрим на следующий вывод для моей системной переменной $PATH:

> echo -e ${PATH//:/\\n}
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Скажем, мы ищем исполняемый файл с именем myspecialexec. Наша система начнется сверху. В этом случае он будет искать в каталоге /usr/local/bin исполняемый файл с именем myspecialexec. Если он находит его, он выполняет его. Если это не так, он переходит к следующему каталогу, указанному в пути - в данном случае /usr/bin, прежде чем перейти к /bin, если он не находит его там, и так далее.

Какое это имеет отношение к Python?

Python — это исполняемый файл. Это то, что вы загружаете (вместе со стандартной библиотекой и некоторыми другими вещами), когда запускаете brew install python или переходите здесь и запускаете установщик python. Вы делаете одно из следующих действий:

  • получение исполняемого файла python и вставка его в один из указанных выше каталогов в $PATH
  • извлекая исполняемый файл python и вставляя его в другое место, но затем гарантируя, что имя этого каталога теперь находится в $PATH

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

Пример 1: Как conda справляется с этим, поэтому мы используем правильную версию python на основе среды conda?

Conda — это, как она сама себя описывает, система управления пакетами с открытым исходным кодом и система управления средой, работающая в Windows, macOS и Linux. Это была первая система, которую я научился создавать и управлять стабильной средой Python, и она приобрела популярность благодаря своей первоначальной привязанности к развитию машинного обучения и статистического программирования в Python (matplotlib, numpy, scipy, pandas, sklearn и т. д.).

Легкая версия, миниконда, — это то, о чем говорит большинство людей, когда говорят о конде. Если бы вы зашли сюда и установили бы его прямо сейчас, как и в случае с python:

  • где-то на вашем компьютере есть исполняемый файл conda
  • этот исполняемый файл будет виден вашей оболочке/терминалу, поскольку он находится где-то в переменной $PATH

Это означает, что когда вы вводите conda в терминале, он будет знать, как выполнить функциональность, поставляемую вместе с conda.

Так как же conda гарантирует, что вы используете нужную версию Python?

Управляя переменной $PATH и, в частности, используя ее свойство "сверху вниз". Чтобы настроить пример, давайте сделаем следующее. Предполагая, что conda установлена ​​и вы открываете новый терминал/оболочку, вы увидите:

(base) >

то есть вы направляетесь в базовую среду conda по умолчанию, которая была автоматически создана для вас. Цель здесь не в том, чтобы говорить о создании окружения, достаточно сказать, что conda пошла и создала где-то «базовый» каталог. И в этом каталоге он поместил:

  • версия python, к которой подключена среда (чтобы каждый раз, когда вы вводите python, когда в базовой среде запускалась одна и та же версия python)
  • все другие зависимости пакетов, которые вы можете загрузить для этой среды

Где именно находится этот каталог? Мы можем увидеть это, используя команду which python:

(base) > which python
/Users/jamisonm/opt/miniconda3/bin/python

Хорошо, отлично. Итак, conda создала где-то каталог и поместила туда версию python, которую мы можем вызывать. Но как наш компьютер узнает, что нужно вызывать эту версию Python, когда мы находимся в этой среде? Потому что он поместил этот каталог вверху $PATH. Мы можем это проверить:

(base) > echo -e ${PATH//:/\\n}
/Users/jamisonm/opt/miniconda3/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Что происходит, когда я деактивирую среду, т.е. я больше не хочу использовать эту среду и соответствующую версию python?

(base) > conda deactivate

> echo -e ${PATH//:/\\n} # environment disabled so lose the (base)
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

> which python
/usr/bin/python

Таким образом, conda deactivate удалил /Users/jamisonm/opt/miniconda3/bin из верхней части системного пути, поэтому по умолчанию мы используем предыдущий поиск сверху вниз и в итоге получаем предустановленный python в /usr/bin/python.

Как насчет создания новой среды? Давайте создадим новую среду под названием conda-demo.

> conda create --name conda-demo
> conda activate conda-demo
(conda-demo) > echo -e ${PATH//:/\\n}
/Users/jamisonm/opt/miniconda3/envs/conda-demo/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Итак, еще раз conda сделал следующее:

  • создал папку где-то (/Users/jamisonm/opt/miniconda3/envs/conda-demo/bin), чтобы вставить версию python, прикрепленную к этой среде (вместе с другими потенциальными загруженными пакетами)
  • поместил этот каталог вверху $ PATH, чтобы, когда мы набираем python, он выполнял python, который существует в этом каталоге, а не один ниже по переменной $PATH

Пример 2: как pyenv справляется с этим, поэтому мы используем правильную версию python на основе указанной среды?

В отличие от conda, pyenv (изначально) не является полноценным менеджером среды, а больше предназначен для решения следующей проблемы:

"Как я могу работать над несколькими проектами на одном компьютере, где используются разные версии Python, и убедиться, что при вводе python я использую ту версию Python, которую намеревался использовать?"

Это инструмент «Управление версиями Python», который принципиально не использует Python. Для установки и управления различными версиями Python важно, чтобы он сам не зависел от Python, а вместо этого в основном представлял собой загрузку скриптов bash.

Как вы, наверное, догадались, управление версиями Python осуществляется посредством манипулирования переменной $PATH. Все немного сложнее, чем conda, поэтому давайте рассмотрим пример. Предполагая, что вы установили его через что-то вроде brew install pyenv, когда вы запускаете новый терминал/оболочку, все должно быть хорошо (потому что оператор (или что-то подобное) eval "$(pyenv init --path)" будет добавлен в конец вашего .zshrc/.zprofile/.bash_profile /.bashrc) и команды pyenv будут загружены в вашу оболочку.

Настроить пример

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

Установите 2 разные версии Python и установите 3.9.7 в качестве глобальной версии

В новом терминале введите следующее, чтобы установить как python 3.8.12, так и python 3.9.7, и установить python 3.9.7 в качестве предпочтительной «глобальной» версии python для использования:

> pyenv install 3.8.12
> pyenv install 3.9.7
> pyenv global 3.9.7

Затем мы можем проверить, что это было успешно, проверив следующее:

> pyenv versions
   system
   3.8.12
 * 3.9.7 (set by /Users/jamisonm/.pyenv/version)

Создайте «новый проект» и установите локальную версию Python 3.8.12

Чтобы оценить, как pyenv может одновременно управлять несколькими версиями Python, нам нужно создать «новый проект» и сообщить ему, что мы не хотим использовать установленную нами «глобальную» версию Python — 3.9.7. Для этого вы можете создать любой новый каталог и запустить следующее:

> mkdir ~/pyenv-demo    # make a new directory called pyenv-demo in my home directory
> cd ~/pyenv-demo       # cd into it
> pyenv local 3.8.12    # set 3.8.12 as the python version to be used in this directory

Что это сделало? Это создало файл .python-version, который содержит только следующий текст: 3.8.12. Если все пойдет по плану, то:

  • когда мы находимся внутри этого каталога (который теперь является простым типом «среды»), мы будем использовать python 3.8.12.
  • когда мы за его пределами, мы будем использовать значение по умолчанию 3.9.7.

Теперь, когда у нас есть это, мы можем проверить, как pyenv гарантирует, что мы используем желаемую версию Python.

Откуда pyenv знает, что нужно запускать эту «локальную» версию Python, когда я набираю python?

Во-первых, давайте переместимся в наш новый каталог, используя cd ~/pyenv-demo. Давайте посмотрим, куда мы указываем, когда набираем python, и имеет ли это смысл в зависимости от нашего пути:

> which python
/Users/jamisonm/.pyenv/shims/python
> echo -e ${PATH//:/\\n}
/Users/jamisonm/.pyenv/shims
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

Таким образом, мы, похоже, используем исполняемый файл python в /Users/jamisonm/.pyenv/shims, и, как и в случае с conda, это связано с тем, что pyenv поместил каталог в начало нашего пути (и поэтому он ищется первым) с именем /Users/jamisonm/.pyenv/shims. Если мы проверим указанный исполняемый файл python, мы поймем, что на самом деле это не настоящий исполняемый файл python, а скрипт bash, называемый «python». Вот что подразумевается под «прокладкой» — мы обманом заставили нашу операционную систему запустить этот исполняемый файл (который называется python, но на самом деле не является python), вместо того, чтобы искать дальше в $PATH и находить настоящий исполняемый файл python.

Так что же делает этот Python-скрипт «shim»?

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

> less /Users/jamisonm/.pyenv/shims/python

#!/usr/bin/env bash
set -e
[ -n "$PYENV_DEBUG" ] && set -x

program="${0##*/}"

export PYENV_ROOT="/Users/jamisonm/.pyenv"
exec "/usr/local/opt/pyenv/bin/pyenv" exec "$program" "$@"

Короче говоря, он проверяет некоторые критерии отладки, устанавливает переменную PYENV_ROOT и затем вызывает другой исполняемый файл: /usr/local/opt/pyenv/bin/pyenv. Итак, до сих пор вместо вызова python все, что он делал, это перехватывал вызов python (путем манипулирования $PATH), а затем вызывал сам себя.

Мы могли проверить этот исполняемый файл (/usr/local/opt/pyenv/bin/pyenv), но он содержит около 150 строк скрипта bash. Вместо этого, предполагая, что все идет хорошо, мы можем сосредоточиться на нижней части, которая имеет:

command="$1"
case "$command" in
"" )
  { pyenv---version
    pyenv-help
  } | abort
  ;;
-v | --version )
  exec pyenv---version
  ;;
-h | --help )
  exec pyenv-help
  ;;
* )
  command_path="$(command -v "pyenv-$command" || true)"
  if [ -z "$command_path" ]; then
    if [ "$command" == "shell" ]; then
      abort "shell integration not enabled. Run \`pyenv init' for instructions."
    else
      abort "no such command \`$command'"
    fi
  fi

  shift 1
  if [ "$1" = --help ]; then
    if [[ "$command" == "sh-"* ]]; then
      echo "pyenv help \"$command\""
    else
      exec pyenv-help "$command"
    fi
  else
    exec "$command_path" "$@"
  fi
  ;;
esac

Вы можете проверить сценарий самостоятельно или можете поверить мне на слово, что это указывает вам на:

  • command_path = /usr/local/Cellar/pyenv/2.2.0/libexec/pyenv-exec (это потому, что я установил pyenv с brew, который помещает этот исполняемый файл в каталог Cellar)
  • "$@" = python

это означает, что теперь мы перешли к еще одному исполняемому файлу, то есть /usr/local/Cellar/pyenv/2.2.0/libexec/pyenv-exec с аргументом python.

Итак, что происходит дальше?

Теперь этот сценарий немного короче, поэтому мы можем распечатать его целиком, а затем пройтись по нему словами:

set -e
[ -n "$PYENV_DEBUG" ] && set -x

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  exec pyenv-shims --short
fi

PYENV_VERSION="$(pyenv-version-name)"
PYENV_COMMAND="$1"

if [ -z "$PYENV_COMMAND" ]; then
  pyenv-help --usage exec >&2
  exit 1
fi

export PYENV_VERSION
PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")"
PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}"

OLDIFS="$IFS"
IFS=$'\n' scripts=(`pyenv-hooks exec`)
IFS="$OLDIFS"
for script in "${scripts[@]}"; do
  source "$script"
done

shift 1
if [ "${PYENV_BIN_PATH#${PYENV_ROOT}}" != "${PYENV_BIN_PATH}" ]; then
  # Only add to $PATH for non-system version.
  export PATH="${PYENV_BIN_PATH}:${PATH}"
fi
exec "$PYENV_COMMAND_PATH" "$@"

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

  • находит файл .python-version в нашем каталоге pyenv-demo
  • читает это для 3.8.12
  • переходит к /Users/jamisonm/.pyenv/versions, где фактически хранятся исполняемые файлы pyenv python.
  • находит тот, который нам нужен, и устанавливает для нашего исполняемого файла значение /Users/jamisonm/.pyenv/versions/3.8.12/bin/python

Вышеописанный процесс описан на своем гите.

Это кажется довольно сложным для того, чего он достигает

Это довольно сложно — особенно если вы программируете на Python и пытаетесь прочитать кучу bash-скриптов. Однако в конечном счете все это скрыто под поверхностью простого вызова pyenv. Иногда приятно понимать точно, что происходит, но в повседневной жизни знать это необязательно. Тем не менее, суть остается — иногда может показаться сложным разобраться в различных программах управления средой и управлении версиями Python, но все сводится к одному: манипулированию переменной $PATH согласованным образом, чтобы нужная версия Python была версией питон вы получите.

Заключение

Надеюсь, после прохождения этого все становится проще. «Управление средой» и «управление версиями Python» на самом деле сводятся к следующему:

  • Управление средой: создайте где-нибудь каталог и вставьте туда версию Python, которую мы хотим использовать с этой средой.
  • Управление версиями Python: убедитесь, что, когда мы находимся в определенных каталогах/средах, путь к прикрепленному исполняемому файлу Python будет найден первым при переходе через $PATH
  • Управление пакетами: загрузите необходимые пакеты в «то же место», что и желаемый исполняемый файл Python, чтобы они использовались вместо пакетов, предназначенных для других сред.

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