Python仮想環境とシェルキャッシュの落とし穴



# はじめに

Pythonで開発を進める際、仮想環境を利用することで依存パッケージのバージョン管理やプロジェクト間の衝突を防ぐことができます。しかし、実際には「仮想環境を有効化しているはずなのに、なぜかグローバル環境のコマンドが実行されてしまう」というトラブルが発生することがあります。このため、他のメンバーのローカル環境では正しくコードが動くのに、自分のローカル環境だけで謎の（プログラム以外の）エラーが発生する場合もあります。

今回は、ローカル環境で以下のようなエラーに悩まされた事例をもとに、
- 仮想環境とグローバル環境の混在
- シェルのPATHとコマンドキャッシュの仕組み  
などの原因と、その解消方法について備忘録として記載します。



# ローカル環境で発生したエラーとその原因

## 1. エラー発生の背景

プロジェクトの `requirements.txt` は以下のようになっていました。

```
streamlit
llama-index
openpyxl
...その他諸々...
docx2txt
```

上記のファイルをもとに、仮想環境（.venv）を作成し、以下のコマンドで依存関係をインストールしました。

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

その後、アプリを起動しようとすると以下のエラーが発生しました。

```python
ModuleNotFoundError: No module named 'docx2txt'
```

しかし、`pip show docx2txt` を実行すると、バージョン0.8が表示され、確実にインストールされていることが確認できました。



## 2. 問題の検証

### 仮想環境の確認

- 仮想環境は正しくアクティベートされており、プロンプトにも `(venv)` と表示されていました。
- しかし、以下のコマンドで実行した結果、実際に使用されている `streamlit` コマンドがグローバルのものを指していました。

  ```bash
  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`）を探すときに参照する**ディレクトリのリスト**です。  
  たとえば、以下のコマンドを実行すると：
  ```bash
  echo $PATH
  ```
  と出力される内容が、$PATH の一覧です。  
  例：
  ```
  /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
  ```

- この場合、シェルはコマンドを探すとき、最初に `/usr/local/bin` を確認し、次に `/usr/bin`、その後 `/bin` の順で探します。  
  たとえば、以下のコマンドを入力した場合：
  ```bash
  streamlit run app.py
  ```
  シェルはまず `/usr/local/bin` に `streamlit` があるか調べ、あればそれを実行します。

#### ② 具体例
- **例1**:  
  `/usr/local/bin` に `streamlit` が存在していた場合、そのコマンドが実行されます。
- **例2**:  
  `/usr/local/bin` に `streamlit` がなければ、次に `/usr/bin` を探して実行します。

---

### 仮想環境を有効化すると $PATH にどのような変化が起きるか

#### ① 仮想環境の作成と有効化
- プロジェクトディレクトリで以下のコマンドを実行して仮想環境を作成し、アクティベートします。
  ```bash
  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` を実行すると、  
  出力例は：
  ```bash
  /Users/あなたのユーザ名/プロジェクトパス/.venv/bin/streamlit
  ```
  となり、仮想環境内の `streamlit` が優先されることが確認できます。



### シェルのコマンドキャッシュについて
- シェル（bash や zsh）は、一度見つけたコマンドのパスを内部キャッシュに保存します。  これにより、同じコマンドを次に実行するとき、すぐにそのパスを返すようになっています。
- しかし、このキャッシュが古い情報のまま残っていると、新たに仮想環境内にインストールされたコマンドが反映されず、以前キャッシュされたグローバル環境のパスが使われ続ける可能性があります。
>私自身、仮想環境をアクティベートすれば必ず仮想環境内のコマンドが使われると思い込んでいたため、エラーの罠にハマりました。つまり、仮想環境に入っていても、シェルのキャッシュがグローバルのコマンドを記憶していると、実際にはグローバルのコマンドが実行されてしまうのです！
この仕組みは一見便利ですが、状況によっては非常に不便な結果を招くため、キャッシュが問題となることを忘れないようにしましょう。

- この場合、`rehash`（または `hash -r`）を実行してキャッシュをクリアすることで、最新の `$PATH` に基づいてコマンドが探し直されます。



## まとめ

- **$PATH** はシェルがコマンドを探すディレクトリリストで、先頭から順にチェックします。
- 仮想環境を有効化すると、その環境の `bin` ディレクトリが `$PATH` の先頭に追加され、仮想環境内のコマンドが優先されます。
- しかし、シェルは一度見つけたコマンドのパスをキャッシュするため、場合によっては古い（グローバルの）パスが使用されることがあります。  
  この場合は `rehash` などでキャッシュをクリアする必要があります。

### それでも解決できない場合は
**・強制的に仮想環境を抜ける**
一旦新しいシェルを開く、または以下のように仮想環境の設定をスキップして新しいセッションを開いてください：
```bash
exec zsh
```


# おわりに

今回の経験から得られた最大の教訓は、**仮想環境を有効化するだけではなく、実際にどのPythonコマンドやモジュールが呼び出されているかを常に確認することの重要性**です。  
シェルの$PATHやキャッシュの仕組みを理解することで、環境トラブルの原因を早期に特定し、解決に導くことができます。

この内容が皆さんのトラブルシューティングの参考になれば幸いです。  
ぜひコメントやフィードバックをお寄せください


https://qiita.com/youril/items/ea5924588eea3c7ffd07