PEP 582で仮想環境が不要になる？

[Goodbye Virtual Environments?](https://medium.com/@grassfedcode/goodbye-virtual-environments-b9f8115bc2b6)という記事に興味を惹かれてPEP 582を試してみました。

## 従来の仮想環境の課題

### 学習コスト

さぁ、Pythonをはじめよう...としたときにまずは仮想環境の作り方から入るのはなかなか大変です。
昨今は[pipenv](https://pipenv-ja.readthedocs.io/ja/translate-ja/)や[Poetry](https://poetry.eustace.io)など高機能なツールがありますが、使い方を覚えるのは容易ではありません。

### ターミナルごとの操作

仮想環境を有効にするためにはターミナルごとにアクティベートなどの作業が発生します。

### 認知的オーバーヘッド

どの場所にどのような環境を構築したのか覚えておく必要があります。

## PEP 582 -- Python local packages directory

先のような課題を解決するために、[PEP 582](https://www.python.org/dev/peps/pep-0582/)が提案されています。PEP 582の概要はつぎのとおりです。

カレントディレクトリ直下に`__pypackages__`が存在する場合は`sys.path`に優先して追加されます。したがって、有効化などの作業が必要ありません。

`__pypackages__`は次のような構造になっています。

```console
.
└── __pypackages__
    └── 3.8
        └── lib
            ├── package_name
```

---

## PEP 582を試す

この提案はまだPythonに取り込まれていませんし、その可能性も不明です[^1]。
このコンセプトを実装した[pythonloc](https://github.com/cs01/pythonloc)をインストールするとPEP 582で提案された機能を使えます。

### pythonlocをインストール

```console
pip install --user pythonloc
```

`pip`コマンドは環境に合わせて`pip3`や`pythono3 -m pip`などに置き換えてください。

インストールすると次のコマンドが`$HOME/.local/bin`に置かれます。

* pythonloc: `python`の代替えコマンド
* piploc: `pip`の代替えコマンド
* pipfreezeloc: `pip freeze`の代替えコマンド

上記コマンドが使えるよう、PATHを通します。

```console
export PATH=$PATH:$HOME/.local/bin
```

ずっと、これを使いたい場合は`.bashrc`などに追加してもよいでしょう。

### パッケージをインストール

動作を比較するために2つのディレクトリを作成します。

```console
mkdir project1 project2
```

`pip`の代わりに`piploc`を使用します。「project1」のディレクトリに`dateutil`をインストールしてみます。

```console
cd project1
piploc install python-dateutil
```

`pip`ではインストール先のディレクトリは`site-packages`ディレクトリの下に置かれますが、「project1」のディレクトリ階層はつぎのようになります。

```console
~/project1$ tree -L 4
.
└── __pypackages__
    └── 3.7
        └── lib
            ├── __pycache__
            ├── dateutil
            ├── python_dateutil-2.8.0.dist-info
            ├── six-1.12.0.dist-info
            └── six.py
```

### いろいろ試してみる

「project1]のディレクトリからさきほどインストールした`dateutil`がインストールされているか確認してみます。`pip freeze`の代わりに`pipfreezeloc`を使います。

```console
~/project1$ pipfreezeloc 
python-dateutil==2.8.0
six==1.12.0
```

`dateutil`をimportしてみます。
`python`コマンドの代わりに`pythonloc`コマンドを使います。

```console
~/project1$ pythonloc -c "import dateutil;print(dateutil)"
<module 'dateutil' from '/home/driller/project1/__pypackages__/3.7/lib/dateutil/__init__.py'>
```

問題なくimportできています。
インストールしていない「project2」のディレクトリで同じコマンドを実行してみます。

```console
~/project1$ cd ../project2
~/project2$ pipfreezeloc
~/project2$
~/project2$ pythonloc -c "import dateutil;print(dateutil)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'dateutil'
```

「project2」ではimportできないことから、「project1」にのみインストールされていることがわかりました。

スクリプトを試してみます。`import.py`というファイルを「project1」に作成して実行してみます。

```import.py
import pprint
import sys

pprint.pprint(sys.path)

import dateutil
print(dateutil)
```

```console
~/project1$ pythonloc import.py
['/home/driller/project1',
 '/home/driller/project1',
 '/home/driller/project1/__pypackages__/3.7/lib',
 '/usr/local/lib/python37.zip',
 '/usr/local/lib/python3.7',
 '/usr/local/lib/python3.7/lib-dynload',
 '/home/driller/.local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/site-packages']
<module 'dateutil' from '/home/driller/project1/__pypackages__/3.7/lib/dateutil/__init__.py'>
```

問題なく実行できます。ほかのディレクトリではどうでしょうか。

```console
~/project1$ cd ../project2
~/project2$ pythonloc ../project1/import.py 
['/home/driller/project1',
 '/home/driller/project2',
 '/home/driller/project1/__pypackages__/3.7/lib',
 '/usr/local/lib/python37.zip',
 '/usr/local/lib/python3.7',
 '/usr/local/lib/python3.7/lib-dynload',
 '/home/driller/.local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/site-packages']
<module 'dateutil' from '/home/driller/project1/__pypackages__/3.7/lib/dateutil/__init__.py'>
```

ディレクトリを移動しても、スクリプトのディレクトリに`sys.path`が登録されているため、importできます。
念の為、ファイルを「project2」にコピーして実行してみます。

```console
~/project2$ cp ../project1/import.py import.py 
~/project2$ pythonloc import.py 
['/home/driller/project2',
 '/home/driller/project2',
 '/home/driller/project2/__pypackages__/3.7/lib',
 '/usr/local/lib/python37.zip',
 '/usr/local/lib/python3.7',
 '/usr/local/lib/python3.7/lib-dynload',
 '/home/driller/.local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/site-packages']
Traceback (most recent call last):
  File "import.py", line 6, in <module>
    import dateutil
ModuleNotFoundError: No module named 'dateutil'
```

「project2」では`dateutil`をインストールしていないため、importできません。


以上の結果から次のことがわかりました。

* 「project1」のディレクトリのみパッケージがインストールされる
* カレントディレクトリから実行することで、アクティベートの作業をしなくともパッケージがimportできる
* スクリプトが置かれたディレクトリに`sys.path`が登録されるため、そのまま実行できる

`sys.path`には`site-packages`が含まれているため、「project1」は完全に独立した環境ではなさそうです。
`site-packages`側にパッケージをインストールしてみます。

```console
~/project2$ pip install pytz --user
Collecting pytz
  Using cached https://files.pythonhosted.org/packages/61/28/1d3920e4d1d50b19bc5d24398a7cd85cc7b9a75a490570d5a30c57622d34/pytz-2018.9-py2.py3-none-any.whl
Installing collected packages: pytz
Successfully installed pytz-2018.9
~/project1$ pip freeze
pythonloc==0.1.1.1
pytz==2018.9
~/project1$ pipfreezeloc 
six==1.12.0
python-dateutil==2.8.0
```

importしてみます。

```console
~/project2$ cd ../project1
~/project1$ pythonloc -c "import pytz;print(pytz)"
<module 'pytz' from '/home/driller/.local/lib/python3.7/site-packages/pytz/__init__.py'>
```

グローバルにインストールしたパッケージがimportできてしまうので、テストなどの用途には向いてなさそうです。

## 雑感

* コンセプトは非常にシンプルで仮想環境の複雑さを解消できそう
* Python本体のコピーを作成しないため、従来の仮想環境より少しエコ
* 完全に独立した環境を作るには工夫が必要？(なにか方法が用意されているかも)
* 複数のPythonバージョンを切り分けたい場合の実装方法が不明

## オマケ

CPythonのバイナリが[pypackageブランチ](https://github.com/kushaldas/cpython/tree/pypackages)から試せます。ビルドして実行してみます。

Pythonのバージョはこんな感じです。

```console
~$ python -V
Python 3.8.0a0
```

`sys.path`を確認してみます。

```console
~$ python -c "import sys, pprint;pprint.pprint(sys.path)"
['',
 '/home/driller/__pypackages__/3.8/lib',
 '/usr/local/lib/python38.zip',
 '/usr/local/lib/python3.8',
 '/usr/local/lib/python3.8/lib-dynload',
 '/usr/local/lib/python3.8/site-packages']
```

`__pypackages__`が`sys.path`に優先して追加されていることがわかります。

パッケージをインストールしてみます。`piploc`に該当するコマンドがないため、`--target`オプションを指定します。

```console
~$ mkdir project1 project2
~$ cd project1
~/project1$ pip install pytz --target="__pypackages__/3.8/lib"
Collecting pytz
  Downloading https://files.pythonhosted.org/packages/61/28/1d3920e4d1d50b19bc5d24398a7cd85cc7b9a75a490570d5a30c57622d34/pytz-2018.9-py2.py3-none-any.whl (510kB)
    100% |████████████████████████████████| 512kB 3.9MB/s 
Installing collected packages: pytz
Successfully installed pytz-2018.9
~/project1$ tree -L 4
.
└── __pypackages__
    └── 3.8
        └── lib
            ├── pytz
            └── pytz-2018.9.dist-info

5 directories, 0 files
```

インストールしたパッケージをimportしてみます。

```console
~/project1$ python -c "import pytz;print(pytz)"
<module 'pytz' from '/home/driller/project1/__pypackages__/3.8/lib/pytz/__init__.py'>
```

pipコマンドからはまだ認識できていないようです。

```console
~/project1$ pip freeze
~/project1$
```

`python`コマンドが`__pypackages__`にインストールしたパッケージを認識しています。
ディレクトリを移動して実行してみます。

```console
~/project1$ cd ../project2
~/project2$ python -c "import pytz;print(pytz)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'pytz'
```

インストールしたパッケージは「project1」にのみインストールされていることがわかりました。

PEP 582が採用された場合、下記の項目はどうなるのか気になるところです。

* pip installのデフォルトのインストール先が`site-packages`から`__pypackages__`に代わるのか？
* デフォルトのインストール先が`site-packages`であるならばpipのオプションが追加されるのか？

[^1]: 2019年2月時点