はじめに
Pythonで開発を進める際、仮想環境を利用することで依存パッケージのバージョン管理やプロジェクト間の衝突を防ぐことができます。しかし、実際には「仮想環境を有効化しているはずなのに、なぜかグローバル環境のコマンドが実行されてしまう」というトラブルが発生することがあります。このため、他のメンバーのローカル環境では正しくコードが動くのに、自分のローカル環境だけで謎の(プログラム以外の)エラーが発生する場合もあります。
今回は、ローカル環境で以下のようなエラーに悩まされた事例をもとに、
- 仮想環境とグローバル環境の混在
- シェルのPATHとコマンドキャッシュの仕組み
などの原因と、その解消方法について備忘録として記載します。
ローカル環境で発生したエラーとその原因
1. エラー発生の背景
プロジェクトの requirements.txt
は以下のようになっていました。
streamlit
llama-index
openpyxl
...その他諸々...
docx2txt
上記のファイルをもとに、仮想環境(.venv)を作成し、以下のコマンドで依存関係をインストールしました。
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
その後、アプリを起動しようとすると以下のエラーが発生しました。
ModuleNotFoundError: No module named 'docx2txt'
しかし、pip show docx2txt
を実行すると、バージョン0.8が表示され、確実にインストールされていることが確認できました。
2. 問題の検証
仮想環境の確認
-
仮想環境は正しくアクティベートされており、プロンプトにも
(venv)
と表示されていました。 -
しかし、以下のコマンドで実行した結果、実際に使用されている
streamlit
コマンドがグローバルのものを指していました。which streamlit # 結果: /Library/Python/3.9/bin/streamlit
重要な発見:python -m streamlit run ...
と streamlit run ...
の違い
-
python -m streamlit run src/backend/main.py
→ この形式で実行すると、アクティブな仮想環境内のPython が使われ、そのPythonにインストールされているモジュール(docx2txtも含む)を正しく参照します。
→ 結果、エラーは発生せず正常に動作しました。 -
streamlit run src/backend/main.py
→ この場合、シェルは$PATH
を使ってstreamlit
を探しますが、キャッシュの影響などでグローバルのstreamlit
が優先的に実行される場合があり、仮想環境内のモジュールが読み込まれずエラーが発生しました。
→ 私は以前からこちらで動作確認していたので、おそらくキャッシュが溜まってしまっていたのだと考えられます。
3. 原因の詳細:シェルの $PATH
とキャッシュの仕組み
以下は、実際の例を交えて「$PATH の基本」と「仮想環境を有効化したときにどのように $PATH が変化するか」を説明した内容です。※ すでに基本的な知識はお持ちの方は読み飛ばしてください。
$PATH の基本
① シェルの動作例
-
$PATH は、シェルがコマンド(例:
streamlit
)を探すときに参照するディレクトリのリストです。
たとえば、以下のコマンドを実行すると:echo $PATH
と出力される内容が、$PATH の一覧です。
例:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
-
この場合、シェルはコマンドを探すとき、最初に
/usr/local/bin
を確認し、次に/usr/bin
、その後/bin
の順で探します。
たとえば、以下のコマンドを入力した場合:streamlit run app.py
シェルはまず
/usr/local/bin
にstreamlit
があるか調べ、あればそれを実行します。
② 具体例
-
例1:
/usr/local/bin
にstreamlit
が存在していた場合、そのコマンドが実行されます。 -
例2:
/usr/local/bin
にstreamlit
がなければ、次に/usr/bin
を探して実行します。
仮想環境を有効化すると $PATH にどのような変化が起きるか
① 仮想環境の作成と有効化
-
プロジェクトディレクトリで以下のコマンドを実行して仮想環境を作成し、アクティベートします。
python -m venv .venv source .venv/bin/activate
-
これにより、シェルは自動的に
$PATH
の先頭に.venv/bin
を追加します。
例えば、アクティベート前の $PATH が:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
であったとすると、アクティベート後は:
/Users/ユーザ名/プロジェクトパス/.venv/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
のように変化します。
② 具体例
- 仮想環境を有効化後、
which streamlit
を実行すると、
出力例は:となり、仮想環境内の/Users/あなたのユーザ名/プロジェクトパス/.venv/bin/streamlit
streamlit
が優先されることが確認できます。
シェルのコマンドキャッシュについて
- シェル(bash や zsh)は、一度見つけたコマンドのパスを内部キャッシュに保存します。 これにより、同じコマンドを次に実行するとき、すぐにそのパスを返すようになっています。
- しかし、このキャッシュが古い情報のまま残っていると、新たに仮想環境内にインストールされたコマンドが反映されず、以前キャッシュされたグローバル環境のパスが使われ続ける可能性があります。
私自身、仮想環境をアクティベートすれば必ず仮想環境内のコマンドが使われると思い込んでいたため、エラーの罠にハマりました。つまり、仮想環境に入っていても、シェルのキャッシュがグローバルのコマンドを記憶していると、実際にはグローバルのコマンドが実行されてしまうのです!
この仕組みは一見便利ですが、状況によっては非常に不便な結果を招くため、キャッシュが問題となることを忘れないようにしましょう。
- この場合、
rehash
(またはhash -r
)を実行してキャッシュをクリアすることで、最新の$PATH
に基づいてコマンドが探し直されます。
まとめ
- $PATH はシェルがコマンドを探すディレクトリリストで、先頭から順にチェックします。
- 仮想環境を有効化すると、その環境の
bin
ディレクトリが$PATH
の先頭に追加され、仮想環境内のコマンドが優先されます。 - しかし、シェルは一度見つけたコマンドのパスをキャッシュするため、場合によっては古い(グローバルの)パスが使用されることがあります。
この場合はrehash
などでキャッシュをクリアする必要があります。
それでも解決できない場合は
・強制的に仮想環境を抜ける
一旦新しいシェルを開く、または以下のように仮想環境の設定をスキップして新しいセッションを開いてください:
exec zsh
おわりに
今回の経験から得られた最大の教訓は、仮想環境を有効化するだけではなく、実際にどのPythonコマンドやモジュールが呼び出されているかを常に確認することの重要性です。
シェルの$PATHやキャッシュの仕組みを理解することで、環境トラブルの原因を早期に特定し、解決に導くことができます。
この内容が皆さんのトラブルシューティングの参考になれば幸いです。
ぜひコメントやフィードバックをお寄せください