Python仮想環境(venv)の仕組み — なぜOS標準のPythonを汚さずに済むのか
Pythonのツールを触っていると、必ずと言っていいほど登場するのが「仮想環境(venv)」だ。python3 -m venv .venv の一行で作られるこのディレクトリは、一体何をしていて、なぜ多くのPythonプロジェクトで当たり前のように使われているのか。今回はvenvが解決している問題と、その内部の仕組みを整理する。
venvが解決している問題 — 「同じPCの中の別プロジェクト」問題
1台のPCで複数のPythonプロジェクトを扱っていると、あるプロジェクトは特定のライブラリのバージョン2系を必要とし、別のプロジェクトは同じライブラリのバージョン3系を必要とする、という状況が起こりうる。OS標準のPython(システムPython)に対して pip install をそのまま実行すると、インストール先はマシン全体で共有される1つの site-packages ディレクトリになるため、後からインストールした方のバージョンで上書きされてしまい、片方のプロジェクトが動かなくなる。
補足:
site-packagesとは、pip installでインストールしたサードパーティ製ライブラリが実際に配置されるディレクトリのこと。Pythonはインポート時にこのディレクトリの中を探しに行く。
さらに、OSによってはシステムPythonがOS自体の内部ツール(パッケージマネージャ等)から使われていることがあり、ここに不用意にライブラリをインストールしたりバージョンを変えたりすると、OSの動作そのものに影響が及ぶ可能性もある。venv(virtual environment・仮想環境)は、この「1つの共有領域に全部積む」構造をやめ、プロジェクトごとに独立した site-packages を持たせることで、この問題を避ける仕組みだ。
venvの内部で何が起きているか
python3 -m venv .venv を実行すると、.venv というディレクトリの中に、おおよそ次のような構成が作られる。
.venv/
├── bin/ # python3・pip の実行ファイル(symlinkまたはコピー)
├── lib/.../site-packages/ # このプロジェクト専用のライブラリ置き場
├── pyvenv.cfg # 元になったシステムPythonの場所などを記録した設定ファイル
ポイントは、.venv/bin/python3 がシステムのPythonそのもの、またはそれへのシンボリックリンクである点だ。つまりvenv内のPythonは「別物のPython実行系」ではなく、同じインタプリタを使い回している。違うのはライブラリの検索先で、.venv/bin/python3 を起動すると、pyvenv.cfg の内容をもとに site-packages の探索先を .venv/lib/.../site-packages に切り替える。この状態で pip install を実行すると、インストール先もこのプロジェクト専用のディレクトリになるため、システムPython側の環境には一切影響しない。
source .venv/bin/activate というおなじみのコマンドは、この仕組みの本質ではなく、あくまで利便性のためのシェルスクリプトだ。実行すると PATH 環境変数の先頭に .venv/bin を追加し、VIRTUAL_ENV という環境変数をセットするだけで、以降ターミナルで python や pip と打つとシステム側ではなくvenv側のものが優先的に見つかる、という状態を作っているに過ぎない。逆に言えば、activate を経由しなくても .venv/bin/python3 script.py のようにフルパスで直接呼び出せば、同じ隔離された環境が手に入る。
自社の実装での実例 — _launcher.sh の起動時venv管理
このアプリのMac/Linux版起動スクリプト _launcher.sh では、まさにこの「activateを経由しない直接呼び出し」を健全性チェックに使っている。
if [ ! -f ".venv/bin/python3" ]; then
NEEDS_SETUP=1
elif [ ! -f "$STAMP" ] || [ "$(cat $STAMP)" != "${ARCH}:${PY_VER}:${REQ_HASH}" ]; then
NEEDS_SETUP=1
elif ! .venv/bin/python3 -c "import sys; sys.exit(0)" >/dev/null 2>&1; then
NEEDS_SETUP=1
fi
このスクリプトは、アプリ起動のたびに .venv を作り直すのではなく、.venv/.install_stamp というファイルに「アーキテクチャ(uname -m):Pythonバージョン:requirements.txt のハッシュ値」という3要素を文字列として保存しておき、次回起動時にこの3要素が前回と一致するかどうかだけを比較している。一致していれば .venv の中身は変わっていないと判断してそのまま再利用し、source .venv/bin/activate だけを実行して起動を続ける。逆に、いずれか1つでも変化していれば(例えばアプリのアップデートで requirements.txt の依存ライブラリが変わった場合や、Intel MacからApple Silicon Macに買い替えた場合など)、.venv を丸ごと削除して python3 -m venv .venv から作り直し、pip install -r requirements.txt を再実行する。
さらに、.venv/bin/python3 -c "import sys; sys.exit(0)" という一見何もしていないように見える行は、venvのPythonバイナリが実際に起動できるかどうかを確認する健全性チェックとして機能している。ここが失敗するのは、例えばOSアップデートでシステムPythonのパスが変わり、venv内のシンボリックリンクが指す先が消えてしまった場合などで、この場合も再構築のトリガーになる。
この「requirements.txtのハッシュが変わっていない限り再構築しない」という設計には、実は1つの前提が隠れている。requirements.txtのバージョン指定記法についての記事で扱ったとおり、このアプリの requirements.txt は flask>=3.0.0 のように == ではなく >= で下限のみを指定している。つまり requirements.txt というファイル自体の中身(ハッシュ)が変わっていなくても、pip install を実行するタイミングによっては、実際にインストールされるライブラリの具体的なバージョンが変わりうる。ハッシュが同じだからといって「venv内の中身が寸分違わず同一である」ことまでは保証されておらず、あくまで「同じ requirements.txt に対して作られたvenvである」ことを保証しているに過ぎない、という点は覚えておく価値がある。
まとめ
venvは、システムPythonという1つの共有領域を複数プロジェクトで奪い合う問題を、プロジェクトごとに独立した site-packages を用意することで解決している。activate はPATHを一時的に書き換える利便性の仕組みにすぎず、隔離そのものは .venv/bin/python3 を直接呼び出すだけで機能する。この性質を利用して、デスクトップアプリの起動スクリプトでは「毎回作り直す」のではなく「環境の変化(アーキテクチャ・Pythonバージョン・依存関係)を検知したときだけ作り直す」という設計にすることで、起動のたびにセットアップを繰り返さずに済むようになっている。