Goodbye Virtual Environments?という記事に興味を惹かれてPEP 582を試してみました。
従来の仮想環境の課題
学習コスト
さぁ、Pythonをはじめよう...としたときにまずは仮想環境の作り方から入るのはなかなか大変です。
昨今はpipenvやPoetryなど高機能なツールがありますが、使い方を覚えるのは容易ではありません。
ターミナルごとの操作
仮想環境を有効にするためにはターミナルごとにアクティベートなどの作業が発生します。
認知的オーバーヘッド
どの場所にどのような環境を構築したのか覚えておく必要があります。
PEP 582 -- Python local packages directory
先のような課題を解決するために、PEP 582が提案されています。PEP 582の概要はつぎのとおりです。
カレントディレクトリ直下に__pypackages__
が存在する場合はsys.path
に優先して追加されます。したがって、有効化などの作業が必要ありません。
__pypackages__
は次のような構造になっています。
.
└── __pypackages__
└── 3.8
└── lib
├── package_name
PEP 582を試す
この提案はまだPythonに取り込まれていませんし、その可能性も不明です1。
このコンセプトを実装したpythonlocをインストールするとPEP 582で提案された機能を使えます。
pythonlocをインストール
pip install --user pythonloc
pip
コマンドは環境に合わせてpip3
やpythono3 -m pip
などに置き換えてください。
インストールすると次のコマンドが$HOME/.local/bin
に置かれます。
- pythonloc:
python
の代替えコマンド - piploc:
pip
の代替えコマンド - pipfreezeloc:
pip freeze
の代替えコマンド
上記コマンドが使えるよう、PATHを通します。
export PATH=$PATH:$HOME/.local/bin
ずっと、これを使いたい場合は.bashrc
などに追加してもよいでしょう。
パッケージをインストール
動作を比較するために2つのディレクトリを作成します。
mkdir project1 project2
pip
の代わりにpiploc
を使用します。「project1」のディレクトリにdateutil
をインストールしてみます。
cd project1
piploc install python-dateutil
pip
ではインストール先のディレクトリはsite-packages
ディレクトリの下に置かれますが、「project1」のディレクトリ階層はつぎのようになります。
~/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
を使います。
~/project1$ pipfreezeloc
python-dateutil==2.8.0
six==1.12.0
dateutil
をimportしてみます。
python
コマンドの代わりにpythonloc
コマンドを使います。
~/project1$ pythonloc -c "import dateutil;print(dateutil)"
<module 'dateutil' from '/home/driller/project1/__pypackages__/3.7/lib/dateutil/__init__.py'>
問題なくimportできています。
インストールしていない「project2」のディレクトリで同じコマンドを実行してみます。
~/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 pprint
import sys
pprint.pprint(sys.path)
import dateutil
print(dateutil)
~/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'>
問題なく実行できます。ほかのディレクトリではどうでしょうか。
~/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」にコピーして実行してみます。
~/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
側にパッケージをインストールしてみます。
~/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してみます。
~/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ブランチから試せます。ビルドして実行してみます。
Pythonのバージョはこんな感じです。
~$ python -V
Python 3.8.0a0
sys.path
を確認してみます。
~$ 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
オプションを指定します。
~$ 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してみます。
~/project1$ python -c "import pytz;print(pytz)"
<module 'pytz' from '/home/driller/project1/__pypackages__/3.8/lib/pytz/__init__.py'>
pipコマンドからはまだ認識できていないようです。
~/project1$ pip freeze
~/project1$
python
コマンドが__pypackages__
にインストールしたパッケージを認識しています。
ディレクトリを移動して実行してみます。
~/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のオプションが追加されるのか?
-
2019年2月時点 ↩