DeepLearning なプロジェクトで Pipenv を導入しようとして感じた良いところ悪いところ

この記事では、 DeepLearning を扱うプロジェクトで pipenv を取り入れようとして良いと思った部分困った部分などを書きます。

# pipenv?
Pipenv 公式ページ： http://pipenv-ja.readthedocs.io/ja/translate-ja/

Pipenv は bundle や npm, yarn など他言語の優秀なパッケージマネージャの世界を python の世界に取り入れようとしているプロジェクトです。

Pipfile に依存するパッケージを書いて `pipenv install` すれば依存関係がインストールされ、その時のバージョンで Pipfile.lock が作られ、同時に外の環境と分離された pip の環境が作成されます。
詳しくは公式ページを御覧ください。

# 良いところ
## バージョンの意図と環境の冪等性を同時に保証できる
- Pipfile がバージョンの意図を表現できる
    - ライブラリAはどのバージョンでもいいけど、ライブラリBは1.5以上で！とか
- Pipfile.lock が冪等性を保証してくれる
    - ライブラリAはどのバージョンでもいいけど、とりあえずみんな2.1を入れときましょうね！を保証できる

従来の requirements.txt 方式だと、 tensorflow==1.5.0 と書かれていたときに、これがどのバージョンでもいいけどたまたま1.5.0を選んだのか、はたまた1.5.0じゃないと動かないので強い意志で1.5.0と指定しているのかわかりません。
逆に tensorflow>=1.5.0 と書かれている場合、1.5.0以上という「意図」は読み取れますが、複数の開発者間で、ある人は1.5.0が入っていてある人は1.6.0が入っていたりして問題の種になります。

Pipenv はこれらの問題を解決します。

## 意図的に入れたライブラリと、意図的にいれたライブラリから副次的に入ったライブラリを区別できる
- Pipenv では意図的に入れたライブラリがどれかが明確です
    - Pipfile: 意図的にいれたライブラリ一覧
    - Pipfile.lock: 意図的に入れたライブラリとそれによって副次的に入れられたライブラリ一覧
- これによって、使ってるか使ってないかわからないゴミライブラリが残存し続けるなどの問題を回避できます

（従来の手法の欠点：）　例えばまっさらな環境に tensorflow を入れて requirements.txt に書き出すことを考えましょう。

```sh
pip install tensorflow
pip freeae > requirements.txt
cat requirements.txt
```

```
absl-py==0.2.2
astor==0.6.2
bleach==1.5.0
gast==0.2.0
grpcio==1.12.0
html5lib==0.9999999
Markdown==2.6.11
numpy==1.14.3
protobuf==3.5.2.post1
six==1.11.0
tensorboard==1.8.0
tensorflow==1.8.0
termcolor==1.1.0
Werkzeug==0.14.1
```

開発の意図として入れたかったのは tensorflow だけですが、他にもいろいろなライブラリが requirements.txt にかかれてしまいました。
後世の人達は、これらのうち、どれが意図的に入れたライブラリなのかわかりません。

いろいろなライブラリを意図的に入れていったあとで、「tensorflow 要らなくなったから消そう」と思っても、ここで入っている absl-py とかは削っていいのか、他のライブラリや開発中のプロジェクトから依存されてないのか調べるのは骨のおれる仕事になるでしょう。

（別の運用方法として、 requirements.txt に tensorflow だけ手で書くという手もありますが、それだと前述した冪等性を保証できません）

Pipenv の場合、 Pipfile には tensorflow のみが書かれ、副次的に入るライブラリたちは Pipfile.lock に書かれますのでこの問題はおこりません。

ここで紹介した2つの良い点によって、 Pipenv を使うとライブラリのアップデートや削除が簡単にできます。


## 独立した環境を自動で切ってくれる
- `pipenv install tensorflow` とすると、 tensorflow は、そのプロジェクトに合わせた独立した環境にインストールされます
    - プロジェクトAは tensorflow1.5を使っていてプロジェクトBはtensorflow1.4を使っているといった場合のオペレーションミスを減らすことができます
        - 違うバージョン入れるなやという話はありますが
    - requirements.txt only な運用の場合、 reqruirements.txt に書かれてないけど他のプロジェクトに寄ってライブラリAがインストールされていたからたまたま動いてしまっていた！みたいなことがよく起こります。

これについては自前で virtualenv 使えば問題ないのですが、そこを自動化してくれるのが Pipenv の嬉しいところです。

## その他良いところ
- 開発環境にのみ入れるライブラリなどの管理ができる
- OS によって入れるものを変えるなどができる
- スクリプト実行コマンドのショートカットが作れる `pipenv run hogehoge`


# 悪いところ
## 環境によって入れるライブラリを変える機能が弱い
具体的には GPU 環境かどうかによって tensorflow と tensorflow-gpu どちらを入れるかを切り替えることができません。
（やり方知ってる人いたら教えてください）

以下に書いてあるように environment marker という環境によってインストールする条件を変える仕組みを使うのが筋に見えますが、残念ながら GPU 環境かどうかの判定はできません。

### 暫定解決方法
現状は CPU only なマシンでも nvidia の cuda/cudnn などのドライバを入れれば実は tensorflow-gpu が動きます。
なので、 CPU マシンも GPU マシンも全部 nvidia のドライバを入れて全部の環境に tensorflow-gpu を入れることになります。

### environment marker が動かない話
pip には environment marker といって、環境によってインストールするライブラリを変える仕組みがあります。
例えば os が windows の場合はこれを入れる、 python のバージョンがこれ以上だったらこれは入れない、など制御ができます。
http://pipenv-ja.readthedocs.io/ja/translate-ja/advanced.html#specifying-basically-anything
[PEP508 指定子](https://www.python.org/dev/peps/pep-0508/)
しかしこいつは **os_name など一部の変数しか扱っておらず、 GPU が使えるかなどは扱えません。**

extra といういかにも外部から値を渡せそうな変数があるので、こいつに外から環境変数経由で GPU マシンかどうかを流し込む案を考えましたが、 **extra はパッケージ側（tensorflow-gpu など）で指定されたものしか受理できない** ことがわりました。

```py3:pipenv/patched/notpip/req/req_set.py
# dist.extras は tensorflow 側。 req_to_install は pipenv などでこちらが指定子たもの。
# 集合の積を取っているので両方に含まれてないと消される。
 709                 available_requested = sorted(
 710                     set(dist.extras) & set(req_to_install.extras)
 711                 )
```
というわけで、 environment marker で GPU 判定させるのは今のところ無理そうです。

## pipenv run python -m hoge.fuga と打つのが面倒
（これは bundler とかイケてるパッケージマネージャもそうなので「慣れろ」という問題です。）

pipenv install でライブラリをインストールしても、それは独立した python 環境にインストールされます。
したがって、その場で python -m hoge.fuga としてもそこでインストールしたライブラリは使えません。

作成した独立環境で python を実行するには大きく分けて以下の２つの方法があります。

### 1. コマンドで pipenv の環境で動作させるよう指示する
```sh
# pipenv run をつける
pipenv run python -m hoge.fuga

# もしくは Pipenv の scripts に hoge = "python -m hoge.fuga" として
pipenv run hoge
```

### 2. 独立した環境の shell を立ち上げる
```
pipenv shell
# これで pipenv が作成した独立した環境にログインできた

python -m hoge.fuga
```
ただし、この方法は個人的には少しイケてなくて、shell を立ち上げてしまっているので、そのプロジェクトのディレクトリを抜けてもその環境が解除されません。
プロジェクトAのディレクトリで pipenv shell したあとプロジェクトBに移動しても、プロジェクトAの環境が引き継がれてしまいます。
個人的には基本1の pipenv run を使うのが良いと思っています。