python

【図解】python仮想環境venvの仕組みと使い方,バージョン指定,切替

python 実行環境と python コマンド

OS に Python をインストールすると実行環境がセットアップされます。

実行環境とは、Python が動作するために必要な一式であり、例えば Python のライブラリ (パッケージ, モジュール) だったり、Python のプログラムコードを解釈するインタープリターだったり、そのインタープリターが実行される環境変数だったりします。

実行環境とプログラムコードを移植すれば、他の OS 上でも基本的に同じ内容が実行されます。

Python 実行環境をインストールする際、他の一般的なアプリケーションインストールとは異なる異質な点があります。それは Python バージョンを混在してインストールできることです。例えば Python version 3.8 と Python version 3.9 を同時にインストールすることができます。

以下の例では Rocky Linux 3 つのバージョンをインストールし、--version で Pythho バージョンを確認、その後 x.py プログラムを実行しています。

$ sudo dnf -y install python3 python38 python39  # 3 つのバージョンをインストール
~~~省略~~~
$ python3 --version 
Python 3.6.8
$ python3 x.py  # python3.6.8 で x.py を実行
Hello, world!
$ python3.8 --version
Python 3.8.12
$ python3.8 x.py  # python3.8.12 で x.py を実行
Hello, world!
$ python3.9 --version
Python 3.9.7
$ python3.9 x.py  # python3.9.7 で x.py を実行
Hello, world!

python と python3 コマンド, pip と pip3 コマンドのリンク

ところで、サンプルプログラムなどでは "python3" , "python3.8" といったコマンドではなく "python" コマンドになってることが多々ありますが、RedHat 系や Ubuntu 等ではデフォルトではこの python コマンドはインストールされません

なので python XXX.py 等と実行しようとすると、以下のようにエラーになります。

-bash: python: コマンドが見つかりません

pip も同様に pip3 コマンドはありますが pip コマンドはインストールされていません

python の Symbolic Link を作る (RedHat 系なら "alternatives --config python" を実行) など、対処方法はいくつかありますが、現在においては次に示す「仮想環境 venv を使う」のが一般的です。

仮想環境は venv だけでなく virtualenv や pyenv 等も有名ですが、いずれも非公式です。公式サポートされている venv が良い選択肢と言えるでしょう。

例えば python 3.9 の仮想環境を作り、そこに入る (activate) することで、python コマンドが使えるようになります。(これは結局 python3.9 への Symbolic Link なのですが、それだけでなく仮想環境のメリットを享受できます。)

python における仮想環境 ~何のための仮想環境?~

仮想環境というと VMware や VirtualBOX 等の「仮想マシン」を思い浮かべるかもしれませんが、Python でいう仮想環境はそれらとは全く異なるもので、「仮想的な実行環境」を指します。

peps.python.org においては仮想環境の目的を以下のように言及しています。

  • 依存関係の管理と分離
  • 一般ユーザー権限での Python パッケージのインストールと使用の容易さ
  • 複数の Python バージョンにわたる Python ソフトウェアの自動テスト

例えば python3.8 の仮想環境を作ると、その仮想環境内では Python プログラムコードは python3.8 で実行され、また、pip でインストールされるパッケージのバージョンも仮想環境内に保持されます。

つまり、仮想環境を複数作り、その環境を切り替えることで、ソースコードをいじらず実行環境だけ変更ができます。例えば python の新バージョンでのテストや新バージョンへの切替を簡単に行うことができるわけです。

仮想環境 venv の作り方と入り方と終了方法

作り方

例えば python 3.8 の仮想環境を作りたい場合、以下を実行します。(ここでは例として仮想環境の名前を .venv38 としています。)

[test@localhost ~]$ python3.8 -m venv .venv38

これだけで仮想環境 .venv38 が作られます。

入り方 (activate)

次に、現在のシェルからこの仮想環境に入る (activate) 手順は以下の通りです。

[test@localhost ~]$ source .venv38/bin/activate
(.venv38) [test@localhost ~]$

これだけで仮想環境に入れます。入った証としてシェルプロンプト (環境変数 PS1) の先頭に (.env38) が付与されます。実際、ここで python --version や pip --version と打つとバージョン情報が表示されます。

入った後に pip でパッケージインストールを実行すると .venv38 ディレクトリ配下の bin や lib 等にインストールされます。

出方 (deactivate)

仮想環境を終了して出たいときは deactivate とするだけです。deactivate は、上記 .venv38/bin/activate シェルスクリプト実行時に読み込まれたシェル関数です (詳細は後述します)。なので activate のときとは違い、パス指定が不要です。

(.venv38) [test@localhost ~]$ deactivate
[test@localhost ~]$

仮想環境 venv の複数作成と切替

前述の通り、仮想環境 venv は複数作成することが可能です。例えば python38 の .venv38 と python39 の .venv39 を 2 つ作っておき、python バージョンおよび pip でインストールした各パッケージのバージョンを一斉に切り替えることが可能です。

図 仮想環境の切替

例えば以下のように 2 つの仮想環境を作成します。

[test@localhost ~]$ python3.8 -m venv .venv38
[test@localhost ~]$ python3.9 -m venv .venv39

前述の通り、仮想環境では pip でインストールされたパッケージはそれぞれ個別に持ちます。

なのでもし .vnenv38 でインストールされた pip パッケージを .venv39 にも移植したいときは以下のように .venv38 にて pip freeze でファイル出力しておき .venv39 で pip install します。(requirements.txt は慣例的に使うファイル名ですが、任意の名前で構いません。)

(.venv38) [test@localhost ~]$ pip freeze > requirements.txt
(.venv38) [test@localhost ~]$ deactivate
[test@localhost ~]$ source .venv39/bin/activate
(.venv39) [test@localhost ~]$ pip install -r requirements.txt

pip freeze では以下のように [package 名]==[version] という形式で表示されます。

(.venv38) [test@localhost ~]$ pip freeze
asgiref==3.5.2
backports.zoneinfo==0.2.1
Django==4.1
sqlparse==0.4.2

ですが、当然 freeze 実行時のバージョンがそのまま残りますので、時間経過により陳腐化してしまいます。==以降のバージョンを削れば pip でサポートされる最新バージョンのパッケージがインストールされます。pip freeze の出力時に削りたいときは以下のコマンドで対応できます。

(.venv38) [test@localhost ~]$ pip freeze | cut -d= -f1 > requirements.txt

また、注意点として、上記の freeze を使った手順では移行が保証されているわけではありません。パッケージの要件が python3.8 と 3.9 で異なる場合があり、エラーとなった場合はエラーの内容を確認して適切な対処が必要になります。

仮想環境 venv のディレクトリ構成

RedHat 系の OS である Rocky Linux に仮想環境を作成すると以下のようなディレクトリが作成されます。

[test@localhost ~]$ ls -al .venv39
合計 8
drwxrwxr-x. 6 test test   87  8月  7 22:01 .
drwx------. 7 test test  198  8月  8 11:13 ..
drwxrwxr-x. 2 test test 4096  8月  7 22:01 bin
drwxrwxr-x. 2 test test    6  8月  7 20:44 include
drwxrwxr-x. 3 test test   23  8月  7 20:44 lib
lrwxrwxrwx. 1 test test    3  8月  7 20:44 lib64 -> lib
-rw-rw-r--. 1 test test   69  8月  7 20:44 pyvenv.cfg
bin ディレクトリ

主に以下のファイルが格納されます。

  • activate 時に使うシェルスクリプト (各 OS/シェルに応じて適切なファイルを実行。例えば bash なら activate, fish なら activate.fish, Windows なら Activate.ps1)
  • python や pip コマンドのシンボリックリンク (python3.9 等を python コマンドで実行)
  • 仮想環境に入った後に pip でインストールされたパッケージに関連したコマンド
lib , lib64 ディレクトリ (lib64 は lib へのシンボリックリンク)

主に以下のファイルが格納されます。

  • 仮想環境に入った後に pip でインストールされたパッケージライブラリ一式
include ディレクトリ

C/C++ で作成した python モジュールを格納するためのもののようです (未確認、詳細求む)。

Python ランタイムサービス sys と site.py

python コマンド (インタプリタ) を実行すると併せて「ランタイムサービス」というものが起動し、自動的に実行環境のセットアップを行います。

Python ランタイムサービス — Python 3.10.6 ドキュメント

ランタイムサービスは標準ライブラリの1つです。

上記 URL にある通り、いくつか種類がありますが、その中でも仮想環境に関係が深い sys と site.py を紹介します。

sys モジュール

sys モジュールは上記 URL にて以下のように説明されています。

インタプリタで使用・管理している変数や、インタプリタの動作に深く関連する関数を定義しています。

sys は「Python の環境変数のようなもの」という理解でよいでしょう。(Linux の環境変数とは違う)

例えばこの後に説明する sys.executable という変数は、python コマンド (インタプリタ) 実行時のディレクトリパスが保存されます。

[test@localhost ~]$ source .venv39/bin/activate
(.venv39) [test@localhost ~]$ python
Python 3.9.7 (default, May 10 2022, 23:45:56)
[GCC 8.5.0 20210514 (Red Hat 8.5.0-10)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.executable
'/home/test/.venv39/bin/python'

site モジュール (site.py)

python は外部からインストールするパッケージを site-packages というディレクトリに保存するのが一般的です。

ですがこの site-packages は場所が 1 か所では無いため、ランタイムサービス site を使って、python 起動時に site-packages の場所の候補を sys.prefix と sys.exec_prefix に保存します。

site --- サイト固有の設定フック — Python 3.10.6 ドキュメント

仮想環境ではこの site.py モジュールを拡張して、仮想環境にインストールしたパッケージが探索されるような仕組みにしています。

この仕組みのカギを握るのが仮想環境ディレクトリ直下にある pyvenv.cfg です。

このファイルは中身も重要ですが、そのファイルの存在自体も重要です。その理由は site.py 内のコメントにも記述されています。sys.executable には python 実行時のパスが入ります。

If a file named "pyvenv.cfg" exists one directory above sys.executable, sys.prefix and sys.exec_prefix are set to that directory and it is also checked for site-packages (sys.base_prefix and sys.base_exec_prefix will always be the "real" prefixes of the Python installation).

具体的に言うと ~/.venv39/bin/python 実行時は以下のように動作し、仮想環境内のパッケージ探索を実現します。

  1. python コマンド (インタープリター) を使ってプログラムコード (*.py) を実行する
  2. 初期化プロセスとして自動的に /usr/lib64/python3.9/site.py が読み込まれ、その内容が処理される
  3. site.py の処理の中で、「実行された python コマンドがあるディレクトリの 1 つ上のディレクトリ (この例では .venv39/bin の 1 つ上なので .venv39) に "pyvenv.cfg" というファイルがあるかどうか」を確認する
  4. もしある場合はそのパス配下の lib/python3.9/site-packages をパッケージの探索対象に追加する

.venv39/bin/python はシンボリックリンクを辿って最終的には /usr/bin/python3.9 が呼び出されますが、もしこれを直接呼び出した場合は上位ディレクトリに pyvenv.cfg は無いため、パッケージ探索対象は追加されません。.venv39/bin/python を実行した場合には site.py の処理によって仮想環境のパッケージが探索対象に追加されるわけです。

仮想環境だけではなく通常の環境にインストールされたパッケージも探索対象に含める場合は仮想環境を作成する際に --system-site-packages オプションを付ける必要があります。

[test@localhost ~]$ python3.9 -m venv --system-site-packages .venv39

これにより、pyvenv.cfg 内に定義された include-system-site-packages が true にセットされ、前述の通り、システムにインストールされたパッケージの探索も実行されます。

このあたりの仕組みは特になのですが、それに限らず Python 仮想環境の詳細動作については以下のえんでぃさんのブログがめちゃめちゃ詳しいので、これを見ながら公式ドキュメントを確認すると良いと思います。

python -m venv [仮想環境名] の処理内容

python -m コマンドはモジュールやパッケージを実行します。

今回の環境では標準パッケージ venv は "/usr/lib64/python3.9/venv" にあります。これはディレクトリになっていて、直下に __main__.py があるのでこれが実行されます。

なのでまずは /usr/lib64/python3.9/venv/__main__.py が実行されますが、その中で __init__.py の main 関数が呼び出され実行されます。

activate の処理内容

以下では Rocky Linux 8.6 の一般ユーザー test の仮想環境 .venv39 で activate するときの例です。下部に .venv39/bin/activate のスクリプトを行数付きで示します。

1 - 36 行目

.venv39/bin/activate を実行するとまず deactivate シェル関数がメモリ上に読み込まれます。

これは前述の通り、仮想環境から抜けるときに、元の環境に戻すために使うコマンドですが、nondestructive 引数を付けることで各種変数や hash テーブルの初期化のみを行うことができるようになっています。

 1 # This file must be used with "source bin/activate" *from bash*
 2 # you cannot run it directly
 3
 4 deactivate () {
 5     # reset old environment variables
 6     if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
 7         PATH="${_OLD_VIRTUAL_PATH:-}"
 8         export PATH
 9         unset _OLD_VIRTUAL_PATH
10     fi
11     if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
12         PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
13         export PYTHONHOME
14         unset _OLD_VIRTUAL_PYTHONHOME
15     fi
16
17     # This should detect bash and zsh, which have a hash command that must
18     # be called to get it to forget past commands.  Without forgetting
19     # past commands the $PATH changes we made may not be respected
20     if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
21         hash -r 2> /dev/null
22     fi
23
24     if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
25         PS1="${_OLD_VIRTUAL_PS1:-}"
26         export PS1
27         unset _OLD_VIRTUAL_PS1
28     fi
29
30     unset VIRTUAL_ENV
31     if [ ! "${1:-}" = "nondestructive" ] ; then
32     # Self destruct!
33         unset -f deactivate
34     fi
35 }
36
37 - 39 行目

各種変数と hash テーブルの初期化を行っています。

37 # unset irrelevant variables
38 deactivate nondestructive
39
40 - 42 行目

環境変数 VIRTUAL_ENV にて仮想環境のフルパスを定義し読み込んでいます。これ自体は共通的な変数として利用しているだけです。(今回は 30 行目と 44 行目にしか使っていないですが、可読性のため)

40 VIRTUAL_ENV="/home/test/.venv39"
41 export VIRTUAL_ENV
42
43 行目

_OLD_VIRTUAL_PATH に現状の環境変数 PATH を保存しています。(deactivate 時に元に戻すのに使うため)

43 _OLD_VIRTUAL_PATH="$PATH"
44 - 46 行目

/home/test/.venv39/bin を PATH の先頭に加え、このディレクトリのコマンドが優先して使われるようにしています。

44 PATH="$VIRTUAL_ENV/bin:$PATH"
45 export PATH
46
47 - 54 行目

仮想環境で邪魔になる環境変数 PYTHONHOME を削除しています。deactivate 時に戻せるように _OLD_PYTHONHOME に保存しています。

47 # unset PYTHONHOME if set
48 # this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
49 # could use `if (set -u; : $PYTHONHOME) ;` in bash
50 if [ -n "${PYTHONHOME:-}" ] ; then
51     _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
52     unset PYTHONHOME
53 fi
54
55 - 60 行目

環境変数 PS1 の先頭に (.venv39) を付け加えています。これにより、プロンプト先頭に (.venv39) が表示されるようになります。

55 if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
56     _OLD_VIRTUAL_PS1="${PS1:-}"
57     PS1="(.venv39) ${PS1:-}"
58     export PS1
59 fi
60
61 ~ 66 行目

hash テーブルに予期せぬコマンドパスがキャッシュされていると不具合を引き起こす可能性があるため、hash テーブルをクリアしています。

61 # This should detect bash and zsh, which have a hash command that must
62 # be called to get it to forget past commands.  Without forgetting
63 # past commands the $PATH changes we made may not be respected
64 if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
65     hash -r 2> /dev/null
66 fi

コメント

タイトルとURLをコピーしました