pyenvとpyenv-virtualenvの自分流使い方

# はじめに

同僚に「Pythonの開発環境って pipenv 一択ですよね」と言われて「いや、自分は pyenv + pyenv-virtualenvで不自由していないんだよな」と思いながらも「こう使えば良いよ」と渡せるものが無かったので書くことにしました。

### 2022.08.11 追記
200LGTMありがとうございます。もうすぐLGTMじゃなくなるみたいですがww、キリ番で[Twitterに投稿](https://twitter.com/qiita_milestone/status/1557622865574461440)していただいたみたいです。なお、私自身はここで紹介させてもらった pyenv + virtualenv から　Poetry に移行していて、それに関しても[「Poetryをサクッと使い始めてみる」](https://qiita.com/ksato9700/items/b893cf1db83605898d8a)で簡単にまとめているので合わせてどうぞ。

# pyenv / virtualenv とは

pyenvおよびpyenv-virtualenvに馴染みのない方に向けて説明しておきます。

まず、pyenvは「一つのマシンに複数のバージョンのpythonをインストールしてそれを切り替えて使える仕組み」です。例えば「新しいプロジェクトでは最新の3.8.5を使うけど、メンテナンスモードに入っている古いプロジェクトでは 3.6.9を使わなければならない」という時に一々Pythonをインストールし直していたら面倒ですよね。そんな時にpyenvを使えば複数のバージョンのPythonをインストールできて、それを切り替えて使うことができます。

そして virtualenv は「あるバージョンのpython環境を複数持つ仕組み」です。これによって「pythonはシステム全体で依存ライブラリを1セットしか登録できない」という問題を解決しようとしています。これが、プロジェクトごとにnode_moduels/というディレクトリ作ってその配下に依存ライブラリをインストールするnode.jsなどと違う部分です。

例えばプロジェクトAはrequestsとpytorchに依存していて プロジェクトBはpandasとnumpyに依存しているとします。プロジェクトAもBも同じバージョンのpythonで動くとするとそこに双方の依存ライブラリをインストールしなければならなくなります。でもそうするとライブラリ同士の衝突が起きてしまったりするかも知れません。それを避けるためにプロジェクトA用のpython3.8.5とプロジェクトB用のpythion3.8.5を作る。それをしてくれるのがvirtualenvです。pyenv-virtualenv はそのvirtualenvをpyenvの中でイイ感じに使えるようにした仕組みです。

# インストール

macOSの人はhomebrewを使うのが一番簡単と思います。

```sh
$ brew update
$ brew install pyenv
$ brew install pyenv-virtualenv
$ echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="${PYENV_ROOT}/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
```

それ以外のOSの方、あるいは最新のpyenvがどうしても使いたいという方は GitHubのリポから直接cloneしてくるというやり方もあります。

pyenvのインストール

```sh
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bash_profile
$ source ~/.bash_profile
```

そして pyenv-virtualenvのインストール

```sh
$ git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
$ echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
```

zshとか他のシェルを使っている方は `~/.bash_profile`の代わりにそれぞれの設定ファイルに読み替えてください。

pyenvにはインストーラーもあるのでそれを使うのも手です。基本は上でやっていることをまとめてやってくれるだけですがお手軽ではあります。

```sh
$ curl https://pyenv.run | bash
```

# 使い方の基本

## Pythonのインストール

pyenvのインストールが無事にできたらまずはpythonをインストールしていきます。その前にまずは利用可能なpythonのバージョンを確認します。

```sh
$ pyenv install --list
```

無印のバージョン番号だけのものが cpythonです。それ以外に、anacondaとかjpythonとかpypyとか色々選べます。その中から自分がインストールしたいバージョンを選んで

```sh
$ pyenv install <python-version>
```

とすればそのバージョンのpythonがインストールされます。

## 利用するPythonを指定する

上記のインストール手順を繰り返せば複数のバージョンのPythonをインストールできます。今現在インストールされているPythonのバージョンを確認したければ

```sh
$ pyenv versions
```

とすればわかります。例えば `3.8.5`と`3.7.9`がインストールされている場合には

```sh
system
3.7.9
3.8.5
```

という感じで表示されます。ここで `system`というのはpyenvではなくてOS側で用意されている pythonになります。　

で、この中から利用するPythonを指定するわけですが、指定にはグローバル、ローカルの二種類があります。グローバルは自分がいるディレクトリに関わらず適用される設定、ローカルは特定のディレクトリ以下に適用される設定です。設定方法は以下のようになります。

グローバル設定

```sh
$ pyenv global <python-version>
```

ローカル設定 

```sh
$ pyenv local <python-version>
```

そして、pythonを起動する際にローカル設定→グローバル設定の順で見に行きます。例えば、

```sh
$ pyenv global 3.8.5
$ cd /Users/johndoe/projectX
$ pyenv local 3.6.9
```

とした場合、`/Users/johndoe/projectX`以下では python 3.6.9 が、それ以外のディレクトリでは3.8.5が使われることになります。

いま、どのバージョンが使われるのかを確認した時には 

```sh
$ pyenv version
```

を使います。例えば上の例だと

```sh
$ cd /Users/johndoe/projectX
$ pyenv version
3.6.9 (set by /Users/johndoe/projectX/.python-version)
$ cd ..
$ pyenv version
3.8.5 (set by /Users/johndoe/.pyenv/version)
```

となります。そして、"set by ... "と書かれているように、実はどのバージョンを使うかはファイルに書かれているだけです。中身を見るとわかりますがこれらは単なるテキストファイルでバージョン番号か書いてあるだけです。pyenvはそれをみて使うpythonのバージョンを特定しています。

## pyenv-virtualenvを使う

次にpyenv-virtualenvの使い方です。まずは

```sh
$ pyenv virtualenv <python-version> <env-name>
```

特定のバージョンのPythonのコピーを作ります。例えば、

```sh
$ pyenv virtualenv 3.8.5 projectA
```

とするとprojectA用の3.8.5が作られます。ここで `pyenv versions`してみると

```sh
system
3.7.9
3.8.5
3.8.5/envs/projectA
projectA
```

と表示されます。今作った（コピーした）環境が利用可能なPythonとして登録されているわけです。`3.8.5/envs/projectA`と`projectA`の2つ作らているように見えますが実体は一つです。

あとは通常のpyenvと同じで、

```sh
$ cd /Users/johndoe/projectA
$ pyenv local projectA
```

とすれば`projectA`を専用の3.8.5環境として使えます。要らなくなったら　

```sh
$ pyenv uninstall projectA
```

とすれば綺麗サッパリ消してくれます。この場合も 3.8.5そのものは残っているので他のプロジェクトで使いたければまた `pyenv virtualenv 3.8.5 ...` とすれば新たな環境を作れます。

# 自分流の使い方

新しくプロジェクトを始めるときはこんな感じで使っています。

```sh
$ mkdir projectZ
$ cd projectZ
$ pyenv virtualenv 3.8.5 projectZ
$ pyenv local projectZ
$ pip install .... (必要なライブラリ)
$ pip freeze -l > requirements.txt
```

こうしてプロジェクトごとに一つ環境を作っています。さらに例えば別のバージョンでの動作確認をしたい場合には

```sh
$ pyenv virtualenv 3.7.8 projecZ-3.7
$ pyenv local projectZ-3.7
$ pyenv install -r requirements.txt
```

とするだけで切り替えができます。

# まとめ

これまで長年使ってきた pyenv + pyenv-virtualenv の解説をしてみました。

これは pyenvの課題というよりもpipの課題なのですが、「依存関係を扱う仕組みが弱い」です。requirements.txtは手動でアップデートしなければならないし、`pip freeze`で作った場合は自分がインストールしたものだけでなくそれが依存しているライブラリも合わせてリストされてしまう。ライブラリのバージョンアップデートをする時も個別に `pip install -U`して変更をrequirements.txtに反映。ちょっとやってられないです。

つまり、「pyenv + pyenv-virtualenvで不自由していない」はあまり正確ではなく「多少不自由しているけど pyenv + pyenv-virtualenvは小回り効くのでなんとかなってた」ということですかね。

そういった問題を解決するために pipenv が出来たわけですが、個人的には [poetry](https://python-poetry.org/) の方が筋が良さそうだなと思っていたりします。そしていくつかのプロジェクトで使ってみてなかなか良さそうなのですが、poetryを入れたところで結局はpyenvは捨てられないし、今のままで良いかなと思っておりました。

が、poetryは内部で venv(python標準の仮想環境作成ツール)を使っていてプロジェクトごとに環境を作ります。ということは pyenv-virtualenv + pip を poetryで置き換えできそうですね。もう少し積極的に使ってみて、これまで出来ていたことが素直にできるのかどうかを確認してみたいと思います。