Pythonのパッケージ管理について(2021年)
はじめに
PyPA
PyPA は Python パッケージングに関する情報、ベストプラクティス、チュートリアルを提供する素晴らしい仕事をしています。そして、彼ら自身のツールである pipenv を推奨しています。 Pythonアプリケーションを開発する際、ライブラリの依存関係を管理するためにPipenvを使用してください。
pipenvの使い方の詳細はManaging Application Dependenciesを参照してください。
PyPAでは、少なくとも2018年以降、依存関係管理の標準ツールとしてpipenvを推奨していますが、pipenvは2017年に始まったばかりであり、その開発スピードも2018年11月から2020年04月の間にpipenvのリリースはありませんでした。
パッケージングの試行錯誤の経緯
Pythonアプリケーションのパッケージ化の主なポイントは、パッケージのメタデータと(ビルド)依存関係を定義することです。これらを定義するファイルには、歴史的な経緯もあり、setup.py、setup.cfg、requirements.txt、Pipfile、pyproject.toml などを目にすることになります。まずは、Python のパッケージングの試行錯誤の経緯を振り返ってみましょう。
setup.py
初期のPythonにはパッケージ管理についての標準的な方法が規定されておらず、当時はソースコードを入手して、インストールすることが主流でした。さまざまなライブラリやツールが開発されていくなかで、インストール方法もプロジェクトにより異なる方法が取られていました。そうしたなか、パッケージを配布するためのモジュール disutills が生まれ、setup.py にインストール方法を設定しておき、ユーザは python setup.py install というコマンドを使うことに統一されました。distutils は distibute、setuptools と別プロジェクトでありながらも発展していきました。
pip
パッケージのソースコードを入手してインストールする方法は、パッケージの数が増えるにつれ明らかに面倒になります。そこで、pip コマンドとPyPI(The Pytohnn Package Index)誕生しました。pip は指定されたパッケージをPyPIからダウンロードし、ZIPファイルを展開し、そこに含まれている setup.py を実行してインストールします。 setup.py の複雑化
パッケージによっては他のパッケージを利用することも多く、これらの依存するパッケージのインストールは手で行うには煩雑すぎるため、setup.py の setup() に install_requires引数が追加されました。この引数に依存パッケージを定義しておくことで、ユーザは pip install パッケージ とだけ実行することで、依存パッケージも同時にインストールできるようになりました。
setup.cfg
次に問題になったのは、setup.py にはパッケージビルド時にカスタマイズできるように任意のPythonコードを含めることができることです。もしsetup.pyにバグや悪意のあるコードが含まれていた場合、これをroot権限で実行することの危険性は説明するまでもないはずです。PyPIリポジトリには誰でも自由にパッケージを登録することができ、いま時点では内容をレビューする仕組みがありません。こうしたことから、setup.cfg にパラメタとして記述するような方法が推奨されるようになりました。setup.cfgのフォーマットは INI形式です。2016年以降、setuptoolsは setup.cfg ファイルを使ったsetup() の設定をサポート していますが、setup.pyファイルを完全に置き換えるものではありません。setup.py の設定のほとんどを setup.cfg に移すことはできますが、編集可能なインストール(pip install -e) をするためには、空の setup() をそのsetup.pyに提供する必要があります。 requirements.txt
パッケージを開発するときに、必要なパッケージを確実に再インストールできることは重要です。これを目的として requirements.txt と pip freezeコマンドが追加されました。
code: bassh
$ pip freeze > requirements.txt
code: bash
$ pip install -r requirements.txt
devel-requirements.txt
例えば pytest などのように、開発時には必要だけれど、実際のリリースでは使用しないようパッケージもあります。これらを区別するために、devel-requirements.txt が追加されました。
wheel
プロジェクトによってはC拡張ライブラリを含んでいたり、システム側のライブラリとリンクするものがあり、コンパイルをユーザ側に負担させていました。wheel は distutils/setuptools を拡張して、バイナリインストールできるようにするものです。クロスプラットフォームのバイナリパッケージは wheel フィアルと呼ばれます。(詳細はPEP 427を参照) pip install --user
Linux のような複数のユーザが同時に使用できるシステムでは、システム側のPythonにパッケージを追加/更新することで、これまで動作していたアプリケーションが動作しなくなる副作用が起きる可能性があります。
pip install時に --userオプションをつけると、パッケージをユーザのホームディレクトリ(ベースバイナリディレクトリ)以下にインストールするため、この問題を回避できます。また、root権限がなくてもインストールすることができることもメリットです。
ベースバイナリディレクトリは次のコマンドで知ることができます。通常は~/.local です。
code: bash
$ python -m site --user-base
venv
setup.py および requirements.txt でバージョンの固定してしまうことで、インストールしようとする他の依存関係と干渉してしまうことが出てきました。これは、パッケージのインストール先が .../site-packages1つだけだということに起因しています。Python仮想環境 venv (virtualenv) が誕生しました。これにより、複数のインストール先が .../site-packagesを持てるようになり、パッケージの干渉問題を回避できるようになりました。
仮想環境 myenv を作る場合: python のバージョンが混在しないように -m venv で作成することが推奨されています。
code: bash
$ python -m venv myenv
仮想環境を有効にする場合:
code: bash
$ source myenv/bin/activate
仮想環境を無効にする場合:
code: bash
$ deactivate
pyenv
ユーザの視点では、複数のPython バージョンを使い分けることの必要性は少ないのですが、プロジェクトを開発する立場では複数のPythonバージョンをサポートすることは重要になります。ここで、Python自体をビルドすることや、Pythonバージョンを切り替えて使用することが非常に面倒になります。こうしたことから pyenv が誕生しました。 pyenv instsall 3.9.7 とすると、Python-3.9.7 をインストールすることができます。pyenv ではホームディレクトリにインストールするので root権限が不要なことも利点のひとつです。
Pipfile + Pipflie.lock
pip freezeで生成されるrequirements.txt はパッケージ名==バージョン を列挙したもので、更新作業が非常に面倒でした。PyPA はpipenv を開発し、 requirements.txt の代わりに Pipfile と Pipfile.lock が追加されました。将来のいつかrequirements.txt を置き換えることになっていますが、今のところ、これらはpipでサポートされていません。また、どのPEP でも言及されていません。pipenv だけがサポートしている状況です。RubyのGemfile と Gemfile.lock に影響されています。 .pypirc
PyPI にパッケージをアップロードするときには、python setup.py upload と行っていました。これは古いPythonではHTTPを使用することになりユーザ名やパスワードがそのまま送出されてしまうセキュリティー上の問題がありました。これを解決するために twine が誕生しました。.pypirc は twine でPyPIにアップロードするときに参照されるファイルです。
pyproject.toml
PEP 518 では、プロジェクトのビルド要件を指定する方法として、pyproject.toml ファイルが規定されました。その後、 PEP 517、PEP 621 、PEP 660 で拡張されました。このファイルにはビルドシステムの要件や情報が含まれており、pip がパッケージをビルドする際に使用されます。 ここで、Pythonはメタデータの設定をTOML形式で行うことになったわけですが、現在PythonにはTOML形式のファイルを読み込むためのモジュールが標準ライブラリにはありません。
pip と setuptools は pyproject.toml をある程度サポートしていますが、まだ setup.py を完全に置き換えるほどではありません。Python の標準的なツールの多くは、すでに pyproject.toml での設定を可能にしているので、このファイルは徐々に setup.cfgや setup.py、requirements.txt に取って代わるようになるでしょう。しかし、いま時点では、まだそこまでには至っていません。
PyPA のガバナンス定義
PyPAは権威を持たない非公式で緩やかな組織で、個人のメンバーシップも明確に定義されていませんでした。この方法は、過去のPyPAではうまく機能していましたが、結果として、明確で透明性のある意思決定プロセスを欠いた組織となっていました。
これを、PEP 609 (現在進行中)で、PyPAのガバナンスモデルを定義することを目指しています。 パッケージ管理ツールの選定
PyPAのガバナンスモデルがまだ確定していない状態ですが、それでも PyPA は Python のパッケージングについて有益な活動をしています。PyPAが推奨している パッケージ管理ツールは次のものとなります。 pipenv
peotry
pip-tools
pip + pyenv
table: パッケージ管理ツールの比較
機能 pip pipenv poetry
リポジトリへのアクセス(PyPI) ✓ ✓ ✓
トップレベルの依存性を記録 X Pipfile pyproject.toml
開発時の依存性を記録 X Pipfile pyproject.toml
すべての依存関係をロック ✓ Pipfile.lock poetry.lock
Pythonバージョンの切り替え X X ✓
リポジトリへの公開 X ✓ (1) ✓
スクリプトの実行 X Pipfile ✓ (2)
変数可能なローカルパッケージ ✓ ✓ ✓
(1):pipenvで直接公開することは、サードパーティの開発依存ファイルと、それを結びつけるスクリプトを使えば可能です。
(2):poetryでスクリプトを実行することは、poethepoet などのサードパーティーツールを使うことで可能です。機能追加される可能性は 議論の推移 から、今のところ低いのですが、プラグインで実現される可能性は残っています。 pipenv
2017 年 1 月に Pipenv というツールが登場しました。Node.js の npm や Ruby の gem の影響を受けたパッケージ管理ツールです。
pipenvを最大限に活用するためには、pyenvがインストールされている必要があります。pipenvは、pyenvと一緒にインストールされたpythonのバージョンを検出し、それを使用することができます。例えば、pipfileにpython 3.4が必要と記載されている場合、pipenv installを正常に実行するためには、pyenv install 3.4.0 を最初に実行する必要があります。
PyPIへ公開することは pipenv ではできません。PyPAでは、PyPIへパッケージ公開には twine の使用を推奨しているため、pipenv でするべきことではないという方針なのでしょう。ただし、PyPiに公開するためにはsetup.pyファイルが必要ですが、pipenv を使ってPipfile から依存関係が自動的に設定されることはありません。
推奨される方法は、公開時に手動で依存関係をコピーするか、Pipfileにに記載されている依存関係をsetup.pyに追加させることですが、pipenv install package_name を実行してもsetup.py は自動的に更新されません。
他にもpipenvには次のような挙動が悪評の原因となっています。
pipenv install package_name とすると、抱えているパッケージすべてを更新してしまう
pipenv install hogehoge など、PyPIに存在しないパッケージを指定しても Pipfile に書き込まれてしまう。
依存関係の解決が弱い。pipenv install oslo.util==1.4.0これは、次のようなメッセージでエラーになります。Could not find a version that matches pbr!=0.7,!=2.1.0,<1.0,>=0.6,>=2.0.0
PyPIのalpha/beta 段階のパッケージをインストールするために install --pre packageとすると、以後すべてパッケージで --preオプションがついた挙動となってしまう。
Poetry
2018年2月にPoetryが開発されました。これは、PEP 518 で提案された pyproject.toml によるパッケージ管理を導入した もので、Rurst の cargo に影響を受けているます。poetry はこれまでパッケージングする際に記述していた setup.py や setup.cfg、MANIFEST.in などのファイルも pyproject.toml に記述できる点で優れています。他にも、linter や formatter の設定を同じファイルに記述できます。つまり、開発者は 依存関係の管理、パッケージ化、公開など、すべての作業を1つの設定ファイルpyproject.tomlで管理し、コマンドラインで処理することができます。
複数の Python プロジェクトを扱うと、必要に迫れれて複数のPython のバージョンが必要になることがあります。Poetry で仮想環境の管理を行うことが出来ますが、複数のPythonバージョンを切り替えることはできないため、pyenv と組み合わせて使用することになります。poetry install や poetry run を実行する前に、使用したいpythonのバージョンを pyenv で有効にする必要があります。
pipenv と poetry の基本的な使用方法
パッケージを追加
pipenv
code: bash
$ pipenv install --dev <package_name> --dev を与えることで開発時の依存パッケージをインストールすることができます。
ローカルパッケージもインストールすることができ、そのパッケージで作業を行い、変更をすぐに確認することができます。
code: bash
$ pipenv install -e /path/to/local_package
poetry
code: bash
$ poetry add --dev <package_name> --dev を与えることで開発時の依存パッケージをインストールすることができます。
pyproject.tomlとpoetry.lockファイルに自動的に追加されます。
ローカルパッケージもインストールすることができ、そのパッケージで作業を行い、変更をすぐに確認することができます。
code: bash
$ poetry add --path /path/to/local_package_dir <npackage_name>
プロジェクト内に venv を作成
Pipenv と Poetry では、初期設定では venv がホームディレクトリ内に作られます。しかし、プロジェクト外のディレクトリにあると管理が面倒になってしまいます。
次の設定を行うことで、プロジェクト内に仮想環境を作成することができます。
pipenv
code: bash
export PIPENV_VENV_IN_PROJECT=1
poetry
code: bash
poetry config settings.virtualenvs.in-project true
poetryについては注意が必要です。この設定を行うと動作が変わり、異なるバージョンのpythonを素早く切り替えることができなくなりますのでご注意ください。たとえ pyenv を使って python のバージョンを切り替えたとしても、poetry run を使って実行されるすべてのコマンドは、ローカルディレクトリに存在する venv (とそれに関連する python のバージョン) を使用します。
異なるバージョンのPythonを切り替える
複数のバージョンのPythonで動作確認を行うには、少しの手順が必要になります。
Poetry は複数のvenvを並べて作成することができるため、この用途には poetry の方がはるかに適しています。
pipenv
code: bash
$ rm -rf /path/to/venv
$ pipenv --python Pythonのバージョン
venvのpythonバージョンがpipfileで指定されたものと一致していないという実害のない警告が表示されるます。他に pyenv を使う方法があります。
poetry
code: bash
$ pyenv global Pythonのバージョン
$ poetry install
新しいvenvを作成して使用するには、pyenvを使ってpythonのバージョンを切り替えてから、新しいvenvを作成するだけです。
pythonのバージョンがpyproject.tomlで指定されたものと一致しない場合はエラーになりますが、semver バージョニング を使ってpythonのバージョンの範囲を指定することができます。 その他のパッケージ管理ツール
DepHell: Pythonのプロジェクト管理。パッケージの管理: フォーマット間の変換、ロック、インストール、解決、隔離、テスト、ビルドグラフ、古くなったものを表示、監査。venvsの管理、ビルドパッケージ、バンプバージョン。 Flit: Python のパッケージやモジュールを PyPI に公開するためのシンプルな方法です。 Maturin: pyo3, rust-cpython, cffi バインディングを使った crate や rust バイナリを python パッケージとして構築・公開します。 Pyflow: Python とパッケージのインストールと依存関係を管理するシステム。 ここでは取り上げていませんでしたが、Pythonのパッケージングについては、まだまだ情勢は流動的です。
所感
Pythonのパッケージングについては試行錯誤の経緯からパッケージ管理ツールも乱立しているような状態ですが、
poetryとpipenvの主な違いは、次の4つになります。
pyproject.tomlの扱い方
pyenvの使い方
PyPiに直接公開できるかどうか
タスク定義したスクリプトを実行できるかどうか
とくにスクリプトについては混乱があります。pipenv では scripts として定義したタスクを実行できる機能があります。poetry での scripts は setup.py に記述する console_scrpts のことで、インストールしたときにエントリーポイントを定義するものです。個人的にはタスク定義と実行については、doit などのタスクランナーに任せてしまう方がスッキリすると考えています。パッケージ管理ツールが注力するべき作業はパッケージングで、モノリスティックな何でもできるツールではないはずですから。 パッケージ管理ツールを使っていると、pyenvを使ってpythonの環境を切り替えることが多くなります。ローカルにインストールしたvenvを使うことで、これに簡単に対応することができます。異なるpython環境で自分のコードをテストするときでは、それを行うための tox のような他のツールがあります。 poetryを使ったPyPiへの公開はとても簡単ですが、pipenvを使ったPyPiへの公開はやはり面倒になってしまいます。
DeepLearning領域で使用されることが多くなったPython ですが、GPUへアクセスするための依存するシステムライブラリ NVIDIA CUDAの肥大化とともに、配布パッケージも巨大化したものが増えてきました。巨大バイナリ配布パッケージに対する、シンプルで比較的高速な依存リゾルバを持つPDMの動向には注目しています。
参考
PyPA ドキュメント