Дълбоко потапяне в това какво се случва, когато напишете „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 и т.н.).

„По-леката“ му версия, „miniconda“, е това, за което повечето хора говорят, когато говорят за „conda“. Ако отидете на „тук“ и го инсталирате, ще го направите сега, точно както с 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“. Това е, което се има предвид под „shim“ – подмамихме нашата операционна система да стартира този изпълним файл (който се нарича 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

Горният процес е описан на their git.

Това изглежда доста сложно за това, което се постига

Доста е сложно - особено идвайки от програмирането на python и опитвайки се да прочете куп bash скриптове. В крайна сметка обаче всичко това е скрито под повърхността на едно обикновено обаждане до pyenv. Понякога е хубаво да разберете точно какво се случва, но всеки ден не е необходимо да знаете това. Въпросът обаче остава - понякога може да изглежда сложно разбирането на различните програми за управление на средата и версиите на python, но всичко се свежда само до едно нещо: манипулиране на променливата $PATH по последователен начин, така че версията на python, която искате, да е версията на python, който получавате.

Заключение

Надяваме се, че от преминаването през това нещата започват да стават по-прости. „Управление на околната среда“ и „версия на Python“ всъщност се свеждат до следното:

  • Управление на средата: създайте директория някъде и залепете там версията на python, която искаме да използваме с тази среда
  • Python версии: Уверете се, че когато сме в определени директории/среди, пътят до прикачения изпълним файл на python се намира първо при преминаване през $PATH
  • Управление на пакети: Изтеглете необходимите пакети на „същото място“ като желания от нас изпълним файл на python, така че да се използват вместо пакети, предназначени за други среди

Последната точка, през която не сме преминали, но след като разберем как компютърът ни знае коя версия на Python да използва, когато напишем python, всъщност изобщо не е много по-сложно. За да го разберем обаче, трябва да разберем пътя за търсене на модула икакто ще видим в следващата статия, това наистина не е много повече от правенето на няколко малки стандартни манипулации към $PATH, които се правят, когато python се стартира.