お伝えしたいこと
Pythonのvenvがどのような仕組みで動いているか気になったので、少し詳しく調べてみました。
ソースコードは読めないので、いつものようにドキュメント上の仕様を追いかけつつ、実機で検証してみました。
今回は、「興味のある方向け」のマニアックな記事です。
あまり興味のない方は、サマリのみご覧ください。
前提知識として、venvの基本的な使い方の理解が必要です。
サマリ
まずは要点を書きます。
これだけで十分かもしれません。
仮想環境ディレクトリ配下の実行ファイル (仮想環境/bin/*
) を実行すると、以下の動きになります。
source 仮想環境/bin/activate
を実行すると、PATHの先頭に仮想環境/bin
が追加されます。
これによりパスを指定せずにコマンド実行すると、/usr/bin
などのシステム全体の実行ファイルよりも仮想環境の実行ファイルが優先的に実行されます。
例えば仮想環境内でansible
コマンドを実行すると、/usr/bin/ansible
よりも仮想環境/bin/ansible
が優先的に実行されます。
そして仮想環境配下の実行ファイルを実行すると、上述の通り仮想環境と紐づくPythonインタプリタとライブラリが参照されるので、諸々が仮想環境配下で動くようになります。
上記の動作を実装しているのがsite.py
というスクリプトであり、その中でも重要な役割を果たすのがsys.executable
やsys.prefix
といった値です。
以降のセクションでより詳細な仕組みを紹介しますので、興味のある方はご覧ください。
PATHの変更
ドキュメント確認
Python仮想環境に入る時、source 仮想環境/bin/activate
コマンドを実行します。
ここで実行しているactivate
は仮想環境作成時に自動生成するシェルスクリプトで、内部的には以下の処理を実行しています。
- PATHの先頭に仮想環境配下のファイル (
仮想環境/bin
) を追記する- 例えば仮想環境に入ったあとに
pip
コマンドを実行すると、/usr/bin/pip
の代わりに仮想環境/bin/pip
が呼ばれるようになる
- 例えば仮想環境に入ったあとに
- 他にも以下の処理をしている
実機確認
前提として、予め~/venv/system
にsystemという名前のPython仮想環境を作成しておきます。
$ python3 -m venv ~/venv/system
仮想環境に入る前後でPATHを比較してみます。
仮想環境に入った後は、PATHの先頭に~/venv/system/bin
が追加されていました。
$ echo $PATH
/home/endy/.local/bin:/home/endy/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
$ source ~/venv/system/bin/activate
(system)$ echo $PATH
/home/endy/venv/system/bin:/home/endy/.local/bin:/home/endy/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
では、~/venv/system/bin
配下のファイルがどうなっているかというと、Shebangで仮想環境配下のPythonインタプリタを指定していました。
そして、仮想環境配下のPythonは、venvを作成した時のPythonへのシンボリックリンクになっていました。
$ head -1 ~/venv/system/bin/pip
#!/home/stopendy/venv/system/bin/python
$ file /home/stopendy/venv/system/bin/python
/home/stopendy/venv/system/bin/python: symbolic link to /usr/bin/python
結局のところは、シンボリックリンク越しに/usr/bin/python
が呼ばれる作りになっています。
そうなると「仮想環境のPythonを実行しても、結局/usr/bin/python
にシンボリックリンクされているなら同じ動作になるじゃないか」と思いたくなるところですが、実は「一旦仮想環境配下のPythonインタプリタのパスを指定して実行することで、仮想環境を利用している扱いになる」ような仕組みがsite.py
に実装されています。
具体的には、sys.executable
に直接実行されたPythonインタプリタのシンボリックリンクのパス (仮想環境/bin/python
) が格納され、それによって「venv内である」と識別しています。
次のセクションに詳細を書きます。
Pythonの実行パスによってprefixを書き換える
ドキュメント確認
sys.prefix
前提知識として、sys.prefix
とsys.exec_prefix
の仕様を紹介します。
sys.prefixとsys.exec_prefixには、Pythonのインストール先のパス (./configure --prefix=...
) がセットされています。
多くの場合、Pythonをソースコードからビルドしてインストールした場合は/usr/local
、Pythonをrpmなどのパッケージからインストールした場合は/usr
がセットされます。
sys.prefix
とsys.exec_prefix
の値は、Pythonスクリプトを実行する度に後述のsite.pyによって書き換えられます。
一方でsys.base_prefixとsys.base_exec_prefix (base_prefixと呼びます) も同じデフォルト値 (/usr
など) を持ちますが、site.pyによって書き換えられることはなく常に同じ値を持ち続けます。
site.pyにおいて上記4種類のprefixがどのような意味を持つかは、次のセクションで詳しく説明します。
site.py
sys.path
は、Pythonパッケージの検索パスを示す値です。
そして、sys.path
の値をセットすることで、Pythonパッケージの検索パスを決めているのがsiteです (ソースコード)。
site.pyの処理の中で、Pythonの実行パスから仮想環境を判定し、sys.path
の値を書き換える処理も書いてあります。
つまり、site.pyの動作を理解することがvenvを理解することに繋がります。
site.pyのコメント部分を読むと、以下の処理をしていると記載がありました。
1つ目の処理は前準備で、2つ目の処理が最も重要です。
sys.executable
(実行したPythonインタプリタへのファイルパス。仮想環境の場合はシンボリックリンクへのファイルパス) の1つ上のディレクトリにpyvenv.cfg
が存在した場合、そのディレクトリのパスがsys.prefix
とsys.exec_prefix
にセットされる- Unix系OSの場合は、
sys.prefix
とsys.exec_prefix
の末尾にlib/pythonX.Y/site-packages
を追記したパスをsys.path配列に追加する pyvenv.cfg
でinclude-system-site-packages = false
以外の値が指定されていた場合は、sys.base_prefix
とsys.base_exec_prefix
で示されるシステムのPythonパッケージも検索対象としてsys.path
の末尾に加わる
実機確認
机上で確認したvenvの仕様を実機上でも確認してみます。
仮想環境に入る前
prefixは、全て/usr
を指しています。
すなわち、Pythonパッケージの検索パスは/usr/lib/python3.9/site-packages/
配下になります。
このことは、sys.path
の値からも読み取れます。
$ python3 -c 'import sys; print(sys.executable)'
/usr/bin/python3'
$ python3 -c 'import sys; print(sys.base_prefix)'
/usr
$ python3 -c 'import sys; print(sys.base_exec_prefix)'
/usr
$ python3 -c 'import sys; print(sys.prefix)'
/usr
$ python3 -c 'import sys; print(sys.exec_prefix)'
/usr
$ python3 -c 'import sys; print(sys.path)'
['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', '/usr/lib64/python3.9/lib-dynload', '/usr/lib64/python3.9/site-packages', '/usr/lib/python3.9/site-packages']
仮想環境に入った後
まずは、pyvenv.cfg
と、仮想環境内のPython実行ファイルの位置関係を確認してみます。
仮想環境を作った時点で、~/venv/system/
配下にpyvenv.cfg
やpython
コマンドへのシンボリックリンクが生成しています。
「python
実行ファイルの上の階層にpyvenv.cfg
が存在する」というPython仮想環境として動作するための条件を満たしています。
つまり、仮想環境配下のPythonを実行すると、sys.prefix
やsys.exec_prefix
が書き換わることになります。
(system)$ file ~/venv/system/pyvenv.cfg ~/venv/system/bin/python3
/home/endy/venv/system/pyvenv.cfg: ASCII text
/home/endy/venv/system/bin/python3: symbolic link to /usr/bin/python3
次に、sys.pathが仮想環境に入ることで変わる様子を確認します。
sys.prefix
とsys.exec_prefix
は仮想環境を反映して/home/endy/venv/system
に変わっています。
これに伴って、Pythonパッケージの検索パス (sys.path
) も仮想環境配下に切り替わっています。
一方で、sys.base_prefix
とsys.base_exec_prefix
は特に値が書き換わることのない仕様のため、変わらず/usr
にセットされたままです。
これによって仮想環境と実環境のPythonパッケージパスの両方を区別しています。
pyvenv.cfg
のinclude-system-site-packages=true
オプションを使う場合、こういった区別が必要になります。
$ source ~/venv/system/bin/activate
(system)$ python3 -c 'import sys; print(sys.exec_prefix)'
/home/endy/venv/system
(system)$ python3 -c 'import sys; print(sys.executable)'
/home/endy/venv/system/bin/python
(system)$ python3 -c 'import sys; print(sys.base_prefix)'
/usr
(system)$ python3 -c 'import sys; print(sys.base_exec_prefix)'
/usr
(system)$ python3 -c 'import sys; print(sys.prefix)'
/home/endy/venv/system
(system)$ python3 -c 'import sys; print(sys.path)'
['', '/usr/lib64/python39.zip', '/usr/lib64/python3.9', '/usr/lib64/python3.9/lib-dynload', '/home/stopendy/venv/system/lib64/python3.9/site-packages', '/home/stopendy/venv/system/lib/python3.9/site-packages']
更に、仮想環境のpyvenv.cfg
について調べてみましょう。
まず、pyvenv.cfg
の中身を見てみます。
include-system-site-packages = false
がデフォルトで記載されているので、この部分を変更しない限りは仮想環境にシステムのパッケージを読み込まないことになります。
(system)$ file ~/venv/system/bin/python3
/home/endy/venv/system/bin/python3: symbolic link to /usr/bin/python3
(system)$ cat ~/venv/system/pyvenv.cfg
home = /usr/bin
include-system-site-packages = false
version = 3.9.0
pyvenv.cfg
のファイル名を変えてみたら仮想環境が認識されなくなり、sys.prefix
が/usr
を指すようになりました。
自動生成しているpyvenv.cfg
は、仮想環境の肝なので誤って削除しないよう気をつけましょう。
(system)$ mv ~/venv/system/pyvenv.cfg ~/venv/system/_pyvenv.cfg
(system)$ python3 -c 'import sys; print(sys.prefix)'
/usr
まとめ
Pythonの根幹にはsite.pyが存在し、Python起動時のコマンドラインのパスによってPythonパッケージ検索先 (sys.path
) を書き換える仕組みを実装していました。
「Python実行ファイル (シンボリックリンク) の一階層上にpyvenv.cfg
ファイルが存在すること」がsys.path
を書き換える条件です。
venvによって仮想環境を生成すると、これらの条件を満たすようにpyvenv.cfg
とpython実行ファイルへのシンボリックリンクを生成します。
そして、source 仮想環境/bin/activate
を実行すると、PATHが書き換わることで仮想環境のシンボリックリンクを参照するようになります。
これによってsys.executable
、sys.prefix
、sys.exec_prefix
、sys.path
などが書き換わり、Pythonでimport
した時のモジュール検索パスが変わっているという原理でした。
本記事では触れていませんが、pip install
実行時のインストール先のディレクトリも上記で書き換わった変数を参照しています。
余談ですが、「仮想環境に入る」という操作をPATHの書き換えによって実現していたということは...やはりsudo pip
はPATHを書き換えるのでご法度です。
sudo
を実行することで、知らず知らずの間に仮想環境の外に出てしまいます。
恐ろしいですね。。
さて、前の記事と併せて、venvの使い方と仕組みについて理解が深まりました。
私自身も含めてですが、これからPythonプログラミングやPython関連ツールの利用を始める方にとってお役に立てれば幸いです。