えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

venvが動作する仕組みを調べてみた

python-logo

お伝えしたいこと

Pythonのvenvがどのような仕組みで動いているか気になったので、少し詳しく調べてみました。
ソースコードは読めないので、いつものようにドキュメント上の仕様を追いかけつつ、実機で検証してみました。

今回は、「興味のある方向け」のマニアックな記事です。
あまり興味のない方は、サマリのみご覧ください。

前提知識として、venvの基本的な使い方の理解が必要です。

サマリ

まずは要点を書きます。
これだけで十分かもしれません。

仮想環境ディレクトリ配下の実行ファイル (仮想環境/bin/*) を実行すると、以下の動きになります。

source 仮想環境/bin/activateを実行すると、PATHの先頭に仮想環境/binが追加されます。
これによりパスを指定せずにコマンド実行すると、/usr/binなどのシステム全体の実行ファイルよりも仮想環境の実行ファイルが優先的に実行されます。
例えば仮想環境内でansibleコマンドを実行すると、/usr/bin/ansibleよりも仮想環境/bin/ansibleが優先的に実行されます。
そして仮想環境配下の実行ファイルを実行すると、上述の通り仮想環境と紐づくPythonインタプリタとライブラリが参照されるので、諸々が仮想環境配下で動くようになります。

上記の動作を実装しているのがsite.pyというスクリプトであり、その中でも重要な役割を果たすのがsys.executablesys.prefixといった値です。
以降のセクションでより詳細な仕組みを紹介しますので、興味のある方はご覧ください。

PATHの変更

ドキュメント確認

Python仮想環境に入る時、source 仮想環境/bin/activateコマンドを実行します。
ここで実行しているactivateは仮想環境作成時に自動生成するシェルスクリプトで、内部的には以下の処理を実行しています。

  • PATHの先頭に仮想環境配下のファイル (仮想環境/bin) を追記する
    • 例えば仮想環境に入ったあとにpipコマンドを実行すると、/usr/bin/pipの代わりに仮想環境/bin/pipが呼ばれるようになる
  • 他にも以下の処理をしている
    • 環境変数VIRTUAL_ENVに仮想環境のディレクトリパスを格納
    • 環境変数PS1を書き換えることで、シェルプロンプトの文字列を変更
    • 仮想環境から抜けるためのdeactivateコマンドを定義

実機確認

前提として、予め~/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.prefixsys.exec_prefixの仕様を紹介します。

sys.prefixsys.exec_prefixには、Pythonのインストール先のパス (./configure --prefix=...) がセットされています。
多くの場合、Pythonソースコードからビルドしてインストールした場合は/usr/localPythonrpmなどのパッケージからインストールした場合は/usrがセットされます。

sys.prefixsys.exec_prefixの値は、Pythonスクリプトを実行する度に後述のsite.pyによって書き換えられます。
一方でsys.base_prefixsys.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つ目の処理が最も重要です。

  1. sys.executable(実行したPythonインタプリタへのファイルパス。仮想環境の場合はシンボリックリンクへのファイルパス) の1つ上のディレクトリにpyvenv.cfgが存在した場合、そのディレクトリのパスがsys.prefixsys.exec_prefixにセットされる
  2. Unix系OSの場合は、sys.prefixsys.exec_prefixの末尾にlib/pythonX.Y/site-packagesを追記したパスをsys.path配列に追加する
  3. pyvenv.cfginclude-system-site-packages = false以外の値が指定されていた場合は、sys.base_prefixsys.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.cfgpythonコマンドへのシンボリックリンクが生成しています。
python実行ファイルの上の階層にpyvenv.cfgが存在する」というPython仮想環境として動作するための条件を満たしています。
つまり、仮想環境配下のPythonを実行すると、sys.prefixsys.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.prefixsys.exec_prefixは仮想環境を反映して/home/endy/venv/systemに変わっています。
これに伴って、Pythonパッケージの検索パス (sys.path) も仮想環境配下に切り替わっています。

一方で、sys.base_prefixsys.base_exec_prefixは特に値が書き換わることのない仕様のため、変わらず/usrにセットされたままです。
これによって仮想環境と実環境のPythonパッケージパスの両方を区別しています。
pyvenv.cfginclude-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.cfgpython実行ファイルへのシンボリックリンクを生成します。
そして、source 仮想環境/bin/activateを実行すると、PATHが書き換わることで仮想環境のシンボリックリンクを参照するようになります。
これによってsys.executablesys.prefixsys.exec_prefixsys.pathなどが書き換わり、Pythonimportした時のモジュール検索パスが変わっているという原理でした。
本記事では触れていませんが、pip install実行時のインストール先のディレクトリも上記で書き換わった変数を参照しています。

余談ですが、「仮想環境に入る」という操作をPATHの書き換えによって実現していたということは...やはりsudo pipはPATHを書き換えるのでご法度です。
sudoを実行することで、知らず知らずの間に仮想環境の外に出てしまいます。
恐ろしいですね。。

さて、前の記事と併せて、venvの使い方と仕組みについて理解が深まりました。
私自身も含めてですが、これからPythonプログラミングやPython関連ツールの利用を始める方にとってお役に立てれば幸いです。