pyenv + poetry (+ pipx) で Python まるっと管理

pyenv + pyenv-virtualenv でヌルヌルの環境に慣れきってしまったデェタサイエンティスト的な感じの人が、poetry でちゃんとアプリ開発環境整備しなきゃね、って場合に役に立つ記事なんじゃないかと。

Python 初めて使う人でも分かるように書いたので、長い記事になっちゃったけど、論点はこんな感じ。

* Python は仮想環境作って使うのがデファクトだろうけど、その管理って何を使う？
    * 用途・目的によって違うよね
    * だから pyenv-virtualenv と poetry を賢く使い分けよう
* パッケージの管理・共有ってどうすれば良い？
    * これも用途・目的によって違うよね
    * requirements.txt と pyproject.toml を基本とするべきじゃね？
    * だから pip/pipreqs/pipx と poetry を賢く使おう

# Python での開発で、管理するもの
そんで（そんで？）、Python なんていう言語で開発してると、色々管理したり切り替えたりしなきゃいけないものがある。

* Python のバージョン
    * 普段は Python 3.8 を使ってるけど、TensorFlow 1.x は Python 3.7 までしか対応してない、なんてことがあるので、バージョン違いの Python 自体を管理して切り替えたい
* Python の仮想環境
    * [車輪の再発明](https://ja.wikipedia.org/wiki/%E8%BB%8A%E8%BC%AA%E3%81%AE%E5%86%8D%E7%99%BA%E6%98%8E)を避ける意味でも、なるべく効率良く確実に実装する意味でも、Python のパッケージはどんどん使っていくべき
    * ただ、やたらめったらパッケージを追加すると、どんどん巨大化するばかりで重くなる
    * よって、積極的に使うが、必要最小限のパッケージで済ませたい
    * もっと言えば、用途によって、これらパッケージ群を使い分けたい
    * という目的で、Python には「仮想環境（venv）」が用意されていて、ある仮想環境にはパッケージAとBが入っていて、別の仮想環境にはパッケージCとDが入っていて、なんてことが出来る
    * この仮想環境を管理して使い分けたい
* Python パッケージ
    * パッケージをコマンド一発でインストール・アンインストールしたい
    * パッケージには依存関係（このパッケージを使うには、あのパッケージも必要）がある場合があるので、それも自動的に解決して欲しい
    * Python で何かを開発する時、そしてそれをリリースする時には、パッケージとして何が必要かを記録して、すべての人に同じ開発環境・実行環境を提供しなければならない

# 便利ツールたち
こういう場合に使える便利ツールを挙げる。（他にもあるかも知らんけど）

* [pyenv](https://github.com/pyenv/pyenv)
    * 様々なバージョンの Python を簡単にインストールしたり切り替えたりすることが出来る
    * `.python-version` というファイルにバージョンの名前を書いておけば、ターミナルでそのファイルがあるディレクトリに移動しただけで、勝手にその Python を使うことが出来る（死ぬほど便利）
    * [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) というプラグインを入れることによって、仮想環境を作成・管理し、普通の Python のバージョンと同じように使うことが出来る
* [virtualenv](https://virtualenv.pypa.io/en/latest/)
    * コアの部分は[venv](https://docs.python.org/3/library/venv.html)として Python のモジュールとして取り込まれた（Python 3.3 以降）けど、まだ健在
    * その名の通り仮想環境を作ったりするやつ
    * 普通は[virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/)と組み合わせて使うと思う
* [pip](https://pip.pypa.io/en/stable/)
    * 言わずとしれたパッケージ管理ツール
    * [PyPI](https://pypi.org/)に収録されたパッケージをインストールしたり、依存関係を解決したりしてくれる
    * 最近は、あまりに PyPI のサーバ負荷が高くなったため `pip search` によるパッケージ検索が出来なくなった
    * 基本的には requirements.txt というファイルにパッケージ一覧を（手動で）書いて、それを共有することで環境共有する
        * [requirements.txtを自動的に作るやつ](https://github.com/bndr/pipreqs)もあるけど
* [pipenv](https://pipenv.pypa.io/en/latest/)
    * pyenv と連携して Python のバージョンを自動ダウンロードするわ、venv 使って仮想環境作るわ、パッケージ管理するわ、と何でもこれひとつで出来る
    * でも、なんか気に食わなくて、私は全然使ってない
    * 何故かと考えると、pipenv は Pipfile と Pipfile.lock を作ってそれで諸々を管理するのだが、勝手にダサい名前のファイルを定義してんじゃねぇ、と
* [poetry](https://python-poetry.org/)
    * この中では一番新しい
    * venv 使って仮想環境作るのと、パッケージ管理
    * 管理するファイルは、pyproject.toml と poetry.lock
    * poetry.lock はともかく、pyproject.toml は [PEP 518](https://www.python.org/dev/peps/pep-0518/) で定義された共通フォーマット
    * pipenv とは違い、Python パッケージを作成する [setuptools](https://setuptools.readthedocs.io/en/latest/) の機能も含む
* [pipx](https://pypi.org/project/pipx/)
    * おまけ
    * パッケージ管理ツールなのだが、特徴は、Python 仮想環境とかに依存しない所にインストールしてくれるところ
    * 要するに、Python で書かれた便利ツールを、いつでもどこでも使うことが出来るようにするもの
    * [awscli](https://aws.amazon.com/jp/cli/) とか [youtube-dl](https://ytdl-org.github.io/youtube-dl/index.html) とか、そういう用途に

機能を比較してみる。

| ツール名 | Python バージョン | 仮想環境管理 | パッケージ管理 | パッケージ作成 |
| :-----: | :-----: | :-----: | :-----: | :-----: |
| virtualenv | | :white_check_mark: | | |
| pip / pipx | | | :white_check_mark: | |
| setuptools | | | | :white_check_mark: |
| pyenv | :white_check_mark: | :white_check_mark: | | |
| pipenv | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
| poetry | | :white_check_mark: | :white_check_mark: | :white_check_mark: |

いっちゃん便利な pipenv 使えよ、って話になりそうなのだが、ちょっと待って欲しい。

# 問題点
（私だけかもしれないが）考えなきゃいけない問題点がいくつかある。

* 仮想環境の使い方って、様々
* パッケージのバージョン指定って、色々
* パッケージリストの共有方法って、諸々

## 仮想環境の使い方
Python ってかなり便利でしかも[流行っている](https://statisticstimes.com/tech/top-computer-languages.php)ので、様々な人が様々な用途で使っているはず。

* 便利ツール/ライブラリを作る/動かす
* アプリを作る/動かす
* データ分析するとか、そういうちょっとしたこと

そういう色々な使い方によって、まずは仮想環境の使い方が違うと思う。

単にツール等を使う場合を除き、Python で何かしようと思った時、その「何か」を「プロジェクト」とする、ってのは Python に限らずもう共通認識だろう。

で、その時に仮想環境をどう使うかなんだけど、私はこうなんだ。

* ツール/ライブラリ/アプリの開発
    * 仮想環境とプロジェクトを 1:1 に紐付けたい
* データ分析
    * ある仮想環境を様々なプロジェクトで使いまわしたい

一応、私はデェタサイエンティストというものらしいので、Python の主な用途はデータ分析なのだが、その時に使う定番パッケージというものがある。

numpy, scipy, scikit-learn, pandas, jupyter, …

これらは全部デカいパッケージだし、分析毎に仮想環境作るってのはちょっと割に合わない。要するに仮想環境を「共通分析基盤」って感じに使いたい。

やったとしても「Tabular データ」「画像処理」「自然言語処理」というように、対象データ毎に共通分析基盤のメンツをちょっと入れ替えるぐらい。

* Tabular データ
    * numpy, scipy, scikit-learn, pandas, jupyter, …
* 画像処理
    * numpy, Pillow, opencv-python, PyTorch, TensorFlow, jupyter, …
* 自然言語処理
    * numpy, mecab-python3, scikit-learn, gensim, jupyter, …

jupyter 入れてる時点で、プロジェクト毎に仮想環境作るってのがもう破綻してるのだが。（jupyter は pipx で入れとくって話はアリだな！）

※ 追記（2021/10/07）
→ [JupyterLab App の代わりに JupyterLab を pipx に入れて使う話](https://qiita.com/tetutaro/items/ba93ab90496490e150cd)

あとは Edge AI 用に TensorFlow Lite が動く環境を別に持っておくとか。

とは言っても、時々アプリも作るわけで、そういう時にはやっぱりプロジェクトと仮想環境を 1:1 にしたい。

で、それぞれの場合で何を使うのが便利かっていうと、

* 仮想環境を複数のプロジェクトで使いまわしたい
    * 仮想環境管理とパッケージ管理が一体になってるのは都合が悪い
    * virtualenv + virtualenvwrapper, pyenv + pyenv-virtualenv
* 仮想環境とプロジェクトを 1:1 にしたい
    * 仮想環境管理とパッケージ管理が一体になっていて欲しい
    * pipenv, poetry

要するに、仮想環境の使い方によってツールを使い分けなきゃいけない。全部ひとつで、って虫の良い話は無い（超残念）。

## パッケージのバージョン指定
「このプロジェクトにはこのパッケージが必要」ってのは、共同開発もそうだし、それを（他人が）使うって場合にも必要だから、絶対に共有しなきゃいけない。

で、その時に、パッケージの一覧はもちろん必須なのだが「どのバージョンなのか」ってのが微妙に難しい。

* 開発中 もしくは ツール・ライブラリの場合
    * 「最低でもこのバージョン以上」っていう情報が必要
* アプリをリリースする場合
    * 「絶対にこのバージョン」っていう情報が必要

要するに[この話](https://kenreitz.org/essays/2016/02/25/a-better-pip-workflow)なのだが。

開発中の時は、まだパッケージは fix してないだろうし、最終的にリリースする時に決めれば良い。

ツール・ライブラリの場合は、色んな環境で使ってもらうことを想定するので、動作を保証するためにも最低のバージョンは制限するとしても、「このバージョンじゃなきゃダメ」って言うのは本末転倒。（back compatibility が保証されない major version のアップデート以外は）

ただし、アプリをリリースする場合、特に docker container 作って動かすアプリみたいな場合、動作を完全に保証するために、minor version も含むバージョンを strict に指定するべきだ。

そして忘れちゃいけないのが、仮想環境を共通基盤として使う場合は、パッケージをインストールする際に一々記録されていても迷惑なんだ。

なので、理想は以下だ。

* 共通基盤とする仮想環境でパッケージをインストールする場合は、特にそのパッケージを記録しない
* プロジェクト毎に独立した仮想環境でパッケージをインストールする場合は、インストールする毎にその名前とバージョンを記録していく
    * インストールしたバージョンを「最低限のバージョン」として
* アプリの開発では無い場合、開発が終了した時点でそのままリリースして良い
    * まぁパッケージの中には minor version のアップデートでも動かなくなっちゃうものもあるかもしんないけど、それはそれ
* アプリの開発の場合、開発が完了した時点で使っているパッケージを一通りアップデートして、動作を確認する
    * そしてパッケージ群を minor version まで fix してリリースする

要するに、また仮想環境の使い方によって、パッケージ管理のツールも違ってくるはずなんだ。

* 仮想環境を複数のプロジェクトで使いまわしたい
    * パッケージの記録を残したくない
    * pip
* 仮想環境とプロジェクトを 1:1 にしたい
    * パッケージの記録を残したい
    * パッケージの一括アップデート、パッケージの fix の機能が欲しい
    * pipenv, poetry

特に poetry における pyproject.toml へのバージョン登録方法（ `numpy = “^1.19.4"` みたいな）は、上記の要求に合った、とても筋が良いやり方だと思う。

## パッケージリストの共有方法
さらに言えば「パッケージ管理にどのツールを使うのか」ってのは、各自の自由だ。

「俺は pipenv を使うから Pipfile, Pipfile.lock を共有する。手前ら全員 pipenv 使って環境再現しやがれ」って言うのは傲慢だと思う。

ということは、パッケージ一覧が記載されたファイルは、何かしら共通のフォーマット、少なくとも human readable なものじゃないとダメだと思う。（個人的見解）

その点 requirements.txt は、ただ１行毎にパッケージの名前を書いていけば良いだけだし、minor version の指定もできる。そもそもちょっと昔（ちょっとだけ！）は pip しかなかった訳で。

requirements.txt はもう共通フォーマットと思って良いと思う。

その意味で、Pipfile, Pipfile.lock は嫌いだ。だから pipenv は嫌いだ。みんながみんな pipenv 使ってると思うなよ。

でも pipenv にも requirements.txt を export する機能（`pipenv lock -r`）があるので、それを使って requirements.txt を共有すれば良いと思う。他人が pipenv を使うことを止めたりはしない。自由なんだ。

それは poetry でも同じで、minor version の strict な指定には poetry.lock が必要。でも poetry にも requirements.txt を export する機能（後述）あるよ。

何よりも「poetry は pyproject.toml を吐き出す」というのが良い。pyproject.toml は、poetry の流儀で言えば、開発環境の設定だ。リリース後にも開発を継続するのに向いている。何より PEP で定義されている共通フォーマットだ。

しかも poetry で pyporject.toml に沿って setuptools 無しでパッケージも作れる。これは便利。

setuptools 使いたい人どうするのかって？ setuptools も [pyproject.toml に対応してる](https://setuptools.readthedocs.io/en/latest/build_meta.html)よ。[PEP 517](https://www.python.org/dev/peps/pep-0517/) で「メタはそういうのから分離すべきだ」って言ってるしね。

# で、結局何をどう使うのか
あくまで個人的見解ね。

* 仮想環境に関係なく共通で使いたい Python ツール
    * pipx
* 様々なPython バージョンの管理
    * pyenv
* 共通基盤とする Python 仮想環境
    * pyenv + pyenv-virtualenv
    * 仮想環境作成
        *  `> pyenv virtualenv xxx yyy`
    * Python パッケージのインストール
        * `> pip install`
    * Python パッケージの一括アップデート（危険）
        * `> pip list —outdated | awk ‘NR>2 {print $1}’ | xargs pip install -U`
    * リリース時の Python パッケージ確定
        * pipreqs
        * requirements.txt を共有
* プロジェクトに紐付ける Python 仮想環境
    * poetry
        * 仮想環境作成
            * `> poetry new` もしくは `> poetry init`
            * 生成された pyproject.toml は共有
            * poetry.lock は .gitignore に入れる
        * Python パッケージのインストール
            * `> poetry add`
        * Python パッケージの一括アップデート
            * `> poetry update`
        * リリース時の Python パッケージ確定
            * `> poetry lock --no-update`
            * `> poetry export -f requirements.txt --output requirements.txt`
            * requirements.txt を共有

なんとか共通化できないものかとは思うが・・・

# 各ツールのインストール・設定
使い方までまとめてしまうと大変なことになってしまうので、インストールと（必要ならば）最低限の設定までをまとめる。

## pipx
どっちにしろ pipx はシステムの Python に依存するので、Homebrew で入れてしまうのが良いと思う。

* `> brew install pipx`
* `> pipx ensurepath`

Mac じゃない場合？知らん。

pipx 自体のアップデートは当然 `> brew upgrade pipx` になる。

## pyenv
Homebrew とかに依存したくないので、[オヒサル](https://github.com/pyenv/pyenv)お薦め通りに独自に入れるのが良いと思う。

* `> git clone https://github.com/pyenv/pyenv.git ~/.pyenv`

こうすると、諸々が `~/.pyenv` 以下に入るわけだが、それで良いんじゃないかなと思う。

pyenv の設定だが、オヒサルの通りにしても上手くいかない。私だけなのか？
オヒサルは以下の設定を `~/.zprofile` に入れろって書いてあるけど私はめんどいので `~/.zshrc` に入れちゃってる。

* `> echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc`
* `> echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc`
* `> echo 'eval "$(pyenv init --path)"' >> ~/.zshrc`
* `> echo 'eval "$(pyenv init -)"' >> ~/.zshrc`

今後、変わる可能性が無きにしもあらず。

pyenv 自体のアップデートは `> cd ~/.pyenv && git pull`。

## pyenv-virtualenv
pyenv を git で入れたので、pyenv-virtualenv も git で入れるべきだろう。

* `> git clone https://github.com/pyenv/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv`

設定も同様。

* `> echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zshrc`

pyenv-virtualenv 自体のアップデートは `> cd ~/.pyenv/pliugins/pyenv-virtualenv && git pull` になるはずだよね（やったことない）。

## pip, pipreqs
まず、pyenv で入れた Python 自体は、一切何も手をつけないことが前提になると思う。何をするにも pyenv-virtualenv か poetry で仮想環境作ってやる、と。

pipreqs をどこに入れるかはちょっと悩むけど、pyenv-vertualenv で作った仮想環境毎に入れるのが良いんじゃないかと思う。pipreqs はプロジェクトを指定して requirements.txt を作るわけだしね。

だから以下のようになるんじゃないかと。

* `> pyenv virtualenv xxx yyy`
	* xxx というバージョンの Python からyyy という名前の仮想環境を作る
* `> pyenv shell yyy	`
	* とりあえず yyy に入る
* `> pip install --upgrade pip pipreqs`

ここらへんは [pyenv-default-packages](https://github.com/jawshooah/pyenv-default-packages) を入れて自動的にやっても良いのかもしれない。

## poetry
~~poetry も Homebrew や pyenv、仮想環境に依存しない形で入れたい。とすると、やはり[オヒサル](https://python-poetry.org/docs/)お薦めの通り git 経由で入れるのが良いのだろう。~~

（2023/01/29 追記） poetry も pipx に入れてしまうという方法がオフィシャルにも出るようになって、目からウロコが落ちた。絶対に pipx に入れた方が良いに決まってる。なので、`> pipx install poetry` だ。

~~で、この時の注意なのだが、仮想環境に依存しないように `> pyenv version` をやって環境が `system` であること、そして Homebrew で入れた方の Python を使おう（気にし過ぎかもしれない）。~~

* ~~`> curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | /usr/local/bin/python3 -`~~

~~そうすると基本的に `~/.poetry` 以下に色々入るわけだが、これで良いんじゃないかと思う。~~

~~後は `~/.zshrc` を編集して PATH 変数に `${HOME}/.poetry/bin` を追加する。~~

（2023/01/29追記）いつのバージョンからだったが、パスは `~${HOME}/.local/bin` になった。素晴らしいことだ。他のアプリの設定で、このパスは既に PATH 変数に入っていることもあるだろう。

poetry 自体の設定は `> poetry config --list` で見れる。この中で変更したいものは `virtualenvs` 関係。

デフォルトの `virtualenvs.in-project` の値は none なので、このままだと仮想環境はキャッシュの下に作られてしまう。これはちょっと気持ち悪い。

ところが、`> poetry config virtualenvs.in-project true` としてしまうと、プロジェクトの直下に `.venv` ディレクトリが作られ、その下に仮想環境が作られる。これは pyenv に慣れてしまうとキツイ。プロジェクトのディレクトリに入ってもいちいち `> poetry shell` と打たなきゃいけないし、他のディレクトリに行っても仮想環境に入ったままだ。`> deactiave` しなきゃいけない。間違いが起こる可能性がある。

ここは思い切って `> poetry config virtualenvs.path "${HOME}/.pyenv/versions"` としてしまおう。

（2023/01/29 追記）現時点でのバージョン 1.3.2 では、`virtualenvs.prefer-active-python` という項目が追加されている。これは自動的に vertualenv に入るという設定ではなかろうか（試してない）。デフォルトは `false` だ。ただ、pyenv に慣れてしまっていることもあり、Python 仮想環境の管理は全部 pyenv にお任せとした方が良いと思う。なので、上記の `virtualenvs.path` の設定変更はした方が良いと思う。そしてさらに `virtualenvs.prompt` という設定項目も追加されている。これが欲しかった！！デフォルトは `"{project_name}-py{python_version}"` なので、これを `"{project_name}"` に変える。

# 新規の仮想環境・プロジェクトを作る
まずは新規に作る場合。

## 共通基盤とする仮想環境
仮想環境の作り方は、再掲だが、以下。

* `> pyenv virtualenv xxx yyy`
    * xxx というバージョンの Python からyyy という名前の仮想環境を作る
* `> pyenv shell yyy	`
    * とりあえず yyy に入る
* `> pip install --upgrade pip pipreqs`
    * pip, pipreqs のインストール

上記を行った後は、`> pyenv shell` しちゃったので、一旦シェル（ターミナル）を落とすのが無難。

新しくプロジェクトを作り、そこに上記の仮想環境を紐付ける場合は

* `> cd <project_home>`
    * 適当に
* git でソースを管理したい場合は、先に git repository を作る
    * 先にディレクトリや何かしらファイルを作ってから git repository を紐付ける方法もあるけど、まぁそこら辺は適当に
* その git repository を clone して、そのディレクトリに行く
    * git を使わない場合は、プロジェクトのディレクトリ掘って行くだけ
* `> pyenv local yyy`
    * `.python-version` ファイルが作られ、そこに yyy と書かれる

要するに `> pyenv local` しろよ、ってだけ。

## プロジェクトに紐付ける仮想環境

紐付けるので、プロジェクトと仮想環境を同時に作る。

* `> cd <project_home>`
    * 適当に
* git でソースを管理したい場合は、先に git repository を作る
    * 先にディレクトリや何かしらファイルを作ってから（略
* その git repositry を clone して、そのディレクトリに行く
    * git を使わない場合は、プロジェクトのディレクトリを（略
* `> pyenv shell xxx`
    * ベースとなるバージョンの Python に入る
* `> poetry init`
    * 対話的に質問されるので、答えていく
    * `> poetry new <project_name>` だと、まだなんか具合が悪い
    * `pyproject.toml` が作られる（だけ）
* `> poetry shell`
    * 何でも良いから poetry のコマンドを叩く
    * `<project_name>-<ID>-<python_version>`という名前の仮想環境が作られる
* `> pyenv local <projet_name>-<ID>-<python_version>`
    * `.python-version` ファイルが作られ、そこに仮想環境名が書かれる
    * これで pyenv の仮想環境と同じ用に使える！！

上記を行った後は、`> pyenv shell` しちゃったので、一旦シェル（ターミナル）を（略

こうやって作った仮想環境は `> pyenv versions` で見える。なので、この仮想環境を消す場合は `> pyenv uninstall` をしなきゃいけない。
そしてこのプロジェクトのディレクトリに出入りすれば、自動的に仮想環境に出入り出来る。
もちろん poetry も使える。

# 既存のプロジェクトから仮想環境を作成・設定する

git clone なり何なりして、既存のプロジェクトをローカルに持ってきて、そのディレクトリに移動したとする。

## 共通基盤である仮想環境を使う場合

* `> pyenv local yyy`
    * `yyy` は使いたい仮想環境名
* `> pip install -r requirements.txt`
    * requirements.txt、あるよね！
    * プロジェクト作った人が pipenv, poetry を使っていた場合、requirements.txt は hash 値も含めた仰々しいものになってるけど、普通に pip で使えるはず

## プロジェクトに紐付けた仮想環境を作る場合（pyproject.tomlが無い場合）

* `> poetry init`
    * `pyproject.toml` が作られる
    * この `pyproject.toml` を repository に加えて良いかは、プロジェクト作った人に確認する必要がある（と思う）
* `> poetry shell`
    * 何かしら poetry のコマンドを叩けば仮想環境を作る筈
* `> pyenv local <project_name>-<ID>-<python_version>`
    * 作られた仮想環境を pyenv local で設定する
* `> cat requirements.txt | xargs poetry add`
    * requirements.txt 、あるよね！
    * poetry は requirements.txt を直接読み込めない
    * なので、１行ずつ poetry add に渡してインストールする

## プロジェクトに紐付けた仮想環境を作る場合（pyproject.tomlがある場合）
* `> poetry shell`
    * 何かしら poetry のコマンドを叩けば仮想環境を作る筈
* `> pyenv local <project_name>-<ID>-<python_version>`
    * 作られた仮想環境を pyenv local で設定する
* `> poetry install`
    * ザクッとインストールする

# 最後に
なんか無理矢理感が否めないし、poetry と pip どっちだっけと間違う可能性もある。（無闇に長ったらしい名前だったら poetry って分かるけど）

poetry はまだまだ開発途中だろうから、今後もっと使いやすくなっていくのだろう。