建立一個方便開發的 Python 環境 (零)- 使用 Pyenv 管理 Python 版本
September 17, 2020

以我第一次拿到 macOS 中的 Python 環境來說, Python 的版本相當多,就像上面這張圖一樣,除了原生系統的 Python 大概就有六隻以外,使用了 Homebrew 還會有 Homebrew 的 Python,可能在某天 brew update 了,預設 Python 還會升級讓過去所有的 Python Soft Link 改變並且導致虛擬環境全部不能 activate,不熟 Homebrew 無奈只好去官方網站下載 Python,此時只看來源不論版本就已經有三個地方會放 Python。再加上每年幾乎都會有不少的功能推陳出新,在 2020-01-01 Python2 還沒有 EOF (End of Life) 之前多多少少會有人還會傾向使用 Python2 開發,同一時刻,許多人已經轉換到使用 3.4, 3.5 甚至 3.6 等較新的版本,然而作為開發者,就會需要同時擁有多個 Python 的版本在機器上來開發不同來源的 Python 專案。至少會需要用來跑 Python2 和 Python3.3+ 的 Python 程式碼,也許原作者使用了 F-String, 還會多需要 Python3.7+ 的 Python 來執行它。
Hmm… 聽起來不太妙,總要一個籠子來好好管理這些蛇,免得早晚被蛇咬到寫不出 code 來 ,Pyenv 就是那個籠子,以下將會介紹 Pyenv 的使用方式及運作原理以及一些不用 Pyenv 的替代方案。
系統 Python 不好嗎
使用系統的 Python 確實會有些問題的:
- 最基本的原因是該版本應該也不會是想要用的版本,現行的版本多為 3.6 - 3.8,稍微舊的系統其內建 Python 多半是 2.7, 3.3, 3.4 等,若有 3.6 可能已經相當堪用,但還是會有下述的問題。
- 由於是系統的 Python,每當要使用 pip 下載任何套件,可能會需要下
sudo pip install的指令,然而一但全部東西都裝在全域,接下來的套件管理也將會是場災難,當這台機器的其他使用者要使用同套件的不同版本時,他將覆蓋先前的版本又或者他根本裝不了,也許帶上--user參數可以緩解這個狀況,但當不同 Project 有著不同的套件版本時,同樣的問題又誕生了,你可能會需要虛擬環境來進行管理,那又會有其他的問題要誕生了。 - 由於是系統的 Python,通常不太敢對它做出更動,以免相依於上面的整個系統壞掉,這也代表著你不能夠完全掌控它,當不可掌控的 Python 因為 OS 更新而發生什麼改變時,可能自己的專案也會跟著不可掌控了。
為什麼要使用 Pyenv
首先 Pyenv 解決了上述有關 Python 版本的全部問題,有著下列的好處
- 可以讓使用者依照自己需求切換全域的 Python 版本
- 可以基於不同專案決定該專案需要的 Python 版本
- 並且不會相依於 Python,是由 shell script 撰寫而成
- 可以透過修改環境變數來覆寫 Python 的版本
另外 Pyenv 本身並沒有提供虛擬環境控管,因此有關套件管理部分尚未得到解決,未來可能還需要自行使用內建的 venv module 或 pyenv-virtualenv plugin 來達成目的。
Pyenv 基本使用方法
安裝 Pyenv (macOS)
安裝 Pyenv (macOS) 需要輸入下面的指令
brew update
brew install pyenv其中也會有下載相依的套件:
brew install openssl readline sqlite3 xz zlib並且依照官方文件教學設定
一般安裝後會在家目錄下產出 .pyenv 的資料夾,其中包含 versions, shims 和plugins,分別會放的內容如下:
versions 資料夾會放置下載的所有 Python 版本
shims 是 pyenv 用於截取使用者呼叫 python 的相關指令,並且將其所附帶的參數一併帶入至 pyenv 執行,shims 路徑會被加入至 PATH 環境變數當中
plugins 資料夾下放置的是 pyenv 相關的插件如管理虛擬環境的 pyenv-virtualenv、檢查安裝環境需求是否有誤的 pyenv-doctor 等。
使用 pyenv init 啟動 shims 及自動補全的功能
在官方文件中有提到,若希望可以讓 shell 啟動 shims 及有自動補全的功能,需要將 pyenv init 指令加入到 shell 配置 (configuration file)中
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bash_profile若是 zsh 的話會需要輸出至
.zshrc
請注意,由於 pyenv init 會改變 PATH 環境變數的內容,使 shell 應該要優先使用 ~/.pyenv/shims 內的指令,請確認該指令位於配置檔案的最下方
完成後,可以重新啟動 shell 讓 PATH 的路徑改變可以重新載入:
exec "$SHELL"pyenv init - 會輸出一些 shell 指令,例如在 zsh 下呼叫下會輸出:(可參考原始碼)
export PATH="/Users/xxxx/.pyenv/shims:${PATH}"
export PYENV_SHELL=zsh
source '/usr/local/Cellar/pyenv/1.2.20/libexec/../completions/pyenv.zsh'
command pyenv rehash 2>/dev/null
pyenv() {
local command
command="${1:-}"
if [ "$#" -gt 0 ]; then
shift
fi
case "$command" in
rehash|shell)
eval "$(pyenv "sh-$command" "$@")";;
*)
command pyenv "$command" "$@";;
esac
}其中做了以下的事情:
- 修改
PATH環境變數,使其加入${PYENV_ROOT}/shims/,讓之後的指令可以優先選擇 shims 中的指令執行 - 新增
PYENV_SHELL環境變數,此變數將會於pyenv rehash及pyenv shell使用 - 導入
pyenv自動補全腳本 - 執行
pyenv rehash安裝 shims
使用 pyenv install 安裝 Python
接著可以輸入 pyenv install <python_version> 來下載想要的 Python 版本,例如想要下載 3.8.0 版的話可以輸入:
pyenv install -v 3.8.0-v 代表會輸出冗長模式說明其中安裝的執行內容,除此以外也可以透過 pyenv install --list 顯示全部可以下載的 Python 版本,可以再透過 grep 輸出想要的版本有哪些
使用 pyenv global <python_version> 設定全域的 Python 版本
舉例來說,輸入 pyenv global 3.8.0 將會設定全域的 Python 版本為 3.8.0,設定後也可以輸入 pyenv global 來確認當前設定的 Python 版本為何。另外設定過後也可以在 .pyenv 上看到多出一個 version 的檔案,其內容為當前設定的全域 Python 版本
使用 pyenv local <python_version> 設定區域的 Python 版本
舉例來說,輸入 pyenv local 3.8.0 將會設定區域的版本為 3.8.0,設定後也同樣可以輸入 pyenv local 來確認當前設定的區域 pyenv 版本為何,並且在設定的該目錄下,可以看到一個 .python-version 檔案,其內容會是該區域的 Python 版本
使用 pyenv versions 顯示已經安裝的 Python 版本
透過輸入 pyenv versions 可以輸出已經下載的所有 Python:
$ pyenv versions
system
* 3.8.0 (set by /Users/xxx/.pyenv/version)
3.8.5pyenv versions 會顯示當下 local 或 global 的所使用的 Python 版本,括號內容為 Python 來源位置,另外若輸入 pyenv version 則不會顯示全部的 Python 可用版本,並只顯示當前使用的 Python 版本,且本地會優先於全域的版本。
使用 pyenv which <command> 得知目前的 command 來源
舉例來說,pyenv which pip3 會顯示當前使用的 pip3 來的來源會是哪個,可能會是系統的 /usr/local/bin/pip3 或是 /Users/xxxx/.pyenv/versions/3.8.0/bin/pip3 等,端看自己透過 pyenv 選用的 Python 版本決定,與 which pip3 差別在於,which pip3 會回傳的是 ~/.pyenv/shims 下的 pip3 而無從得知 pyenv 選擇的版本為何。
使用 pyenv uninstall <python_version> 解除安裝指定的 Python 版本
假設要刪除 Python 3.8.5 的話,只需要輸入 pyenv uninstall 3.8.5,則 pyenv 會刪除 ~/.pyenv/versions/3.8.5
Pyenv 其他使用方法
除了上述安裝、解除安裝、在不同 Scope 切換不同的 Python 版本以外,以下還有一些比較特別的使用方法
使用 pyenv shell
在 shell 配置檔末加入 pyenv init - 將可以自動載入 pyenv,接著才能夠使用 pyenv shell
pyenv shell <verison> 將可以設定 PYENV_VERSION 環境變數,作為 shell 使用的 Python 版本,此版本將會覆蓋全域及區域的 Python 版本,若不需要可以使用 pyenv shell --unset 取消。
當想要在 shell 中使用多個版本的 Python 的話,可以輸入 pyenv shell <version>...,輸入後便可以在環境中使用 Python 的版本,舉例來說,想要可以使用 2.7.7 和 3.8.0 的話可以輸入 pyenv shell 2.7.6 3.8.0,接著可以看到下面的結果:
$ pyenv versions
system
* 2.7.6 (set by PYENV_VERSION environment variable)
* 3.3.3 (set by PYENV_VERSION environment variable)
$ python --version
Python 2.7.6
$ python2.7 --version
Python 2.7.6
$ python3.3 --version
Python 3.3.3輸入比較前面的版號將會為是優先使用的 Python 版本,所以指令 python 會使用 2.7.6
另外,pyenv global 和 pyenv local 同樣接受多個版本號的參數,作用如同 pyenv shell 輸入多個版本號,差別在於 pyenv shell 版本將會覆蓋 pyenv local 版本,並且 pyenv local 版本會覆蓋 pyenv global 版本。
使用 pyenv rehash
pyenv rehash 是在每次下載新的 Python 版本時,用於更新 Shims 可以使用的指令,其實在 pyenv init 和 pyenv install 中都會在執行這個指令,供使用者方便使用。
淺析 Pyenv 原理
如同前面介紹 pyenv init 時提及的, pyenv 將會修改 PATH 這個環境變數,要了解 Pyenv 的運作怎麼切換不同版本的 Python 首先要先了解 PATH 環境變數,以及 Shims 在 Pyenv 中扮演的角色為何。
PATH 環境變數
當想要在 shell 中執行任何指令時,系統首先要知道這些指令是什麼,然而系統便會去一個個的路徑尋找相同名字的可執行檔案,而這些路徑將會首先定義在 PATH 環境變數中,若在 shell 中執行 echo $PATH 將可以看到一串由冒號分隔的字串,例如:
/Users/xxx/.pyenv/shims:/usr/local/opt/llvm/bin:/Users/xxx/torch/install/bin:/Library/Frameworks/Python.framework/Versions/3.5/bin:/opt/local/bin/:/Users/xxx/bin系統將會由左至右開始查找,因此在前面的目錄先找到的話便不會往下繼續找,而當輸入 eval "$(pyenv init -)" 時會將把 ${PYENV_ROOT}/shims 加入 PATH 的最前面,因此達到呼叫 Pyenv shims 中的指令而非系統的。
Shim 是什麼
Shim 在維基百科的解釋是:
In computer programming, a shim is a library that transparently intercepts API calls and changes the arguments passed, handles the operation itself or redirects the operation elsewhere.
大意指的是 Shim 的主要工作就是擷取 API 呼叫並且改變其中的參數,隨後將改變後的參數傳給其他執行單元執行、或自身處理。
而在 ${PYENV_ROOT}/shims中的每支腳本都是做這樣的事情(Pyenv 稱之為 rehashing),其中的程式碼如下:
#!/usr/bin/env bash
set -e
[ -n "$PYENV_DEBUG" ] && set -x
program="${0##*/}"
if [[ "$program" = "python"* ]]; then
for arg; do
case "$arg" in
-c* | -- ) break ;;
*/* )
if [ -f "$arg" ]; then
export PYENV_FILE_ARG="$arg"
break
fi
;;
esac
done
fi
export PYENV_ROOT="/Users/wilson/.pyenv"
exec "/usr/local/Cellar/pyenv/1.2.20/libexec/pyenv" exec "$program" "$@"其中可以看到最後一行會將輸入的指令及參數帶入至 pyenv exec 執行,這些程式碼也是在 pyenv rehash 時建立於 ${PYENV_ROOT}/shims 下的。
pyenv exec 在做什麼
查看原始碼可以發現,下面者一段:
PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")"
PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}"
# ...
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" "$@"其中透過呼叫 pyenv which 可以得出當前使用的 Python 版本,並且取得其 bin/ 位置加入至 PATH 環境變數,最後再帶入原先帶入的參數執行。
替代方案
- 若不希望使用 pyenv 的 Shim,也不希望看到像是
.python-version,version,PYTHON_VERSION這樣的檔案或變數在,也可以透過 pyenv 中的 python-build 來幫助自己下載 特定的 Python 版本並解壓縮、編譯到想要的位置。 擷取自 PyConTW’18 TP 大大的分享:
$ python-build 3.6.5 ~/.local/pythons/3.6
$ python-build 3.5.4 ~/.local/pythons/3.5
$ ln -s ~/.local/pythons/3.6/python3.6 ~/.local/bin
$ ln -s ~/.local/pythons/3.5/python3.5 ~/.local/bin
$ ln -s ~/.local/bin/python3.6 ~/.local/bin/python3- 若不使用 Pyenv 也完全不希望使用系統的 Python 版本,可以將下列指令放入 shell 配置檔中,同樣取自 PyConTW’18 TP 大大的分享:
python() {
local PYTHON="$(which python)"
if [[ "$PYTHON" == /usr/* ]];
then
echo "nope" >&2 | echo >/dev/null
else
"$PYTHON" "$@"
fi
}如此便可以避免呼叫到系統的 Python 版本。
參考資料
- 這樣的開發環境沒問題嗎? (— TP@PyConTW’18 ) (slides)
- Managing Multiple Python Versions With pyenv
- Pyenv
- Pyenv Commands
- Deep dive into how pyenv actually works by leveraging the shim design pattern
- PATH (variable)
- Shim Wiki
其他備註
- Shell Parameter Expansion (看原始碼會需要知道一些 Expansion 的意思)