Python パッケージングの標準を知ろう

こんにちは、CET チームの田村真一です。リクルートライフスタイル Advent Calendar 2019 最終日の記事をお届けします。

本記事では Python のパッケージングに焦点を当て、2019年末時点で

  • ライブラリのパッケージングについて
  • 標準がどう定められているのか

を紹介します。

逆に言うと

  • アプリケーションの依存管理の話
  • ベストプラクティスや便利なツールの紹介
  • Pipenv と Poetry どちらを使うべきか論争

一切しませんのでご了承ください。

そもそも「標準」とは

さて、本題に入る前に Python における「標準」とは何なのか確認しておきましょう。

まず言語仕様に関していえば、C や Ruby のような国際規格に則った「仕様」は存在しません。 かわりに language referenceCPython の実装 がデファクトスタンダードとなっています。

いっぽうで、Python における「標準」を定めるためのものとして PEP (Python Enhancement Proposal) があります。 PEP で採択されたら標準になるというわけですね。 ちょうどインターネットにおける RFC (Request for Comment) のようなものです。

有名所でいえば

あたりでしょうか。さまざまなことが PEP で決まっていることが分かりますね。

なおそれぞれのステータスは、実はさまざまです。 PEP 8 や PEP 373 は「永遠に完成しない」active ステータス。 PEP 572 は「受領されたが標準実装には入っていない」accepted ステータス(final の1つ手前)。 そして PEP 484 は「基本的には accepted だが、final になる前にユーザーからのフィードバックを必要とする」provisional (=“provisionally accepted”) ステータス。 各ステータスの意味と遷移図は PEP 1 を参照してください。

パッケージングの歴史

これに対しパッケージングに関するプラクティスは、従来はあまり PEP でまとめられてこなかったのも事実です。 そこで、まずは Python パッケージングの歴史を追ってみることにしましょう。

最初に登場したのは distutils です。

これは Python に標準ライブラリとして含まれているパッケージングツールです。 登場した当時は無論これしかなく、インストールツールは何もないという状況でした。 現在ではその存在が意識されることはありませんが、今でも各種ツールのコアで使われ続けています。

次に登場したのがパッケージングのための setuptools とそれに付属するインストールツール easy_install です。 setuptoolsdistutils を拡張したもので人気を博しました。 これらは標準ライブラリではなく、PyPA というワーキンググループによって開発されています。

そういえば、setuptools が開発停滞していたころにフォークされた distribute というツールもありましたね。 これは現在では setuptools にマージされていますので、もう存在しません。

最後発で登場したのが、皆さんご存知 pip です。 これは easy_install をさらに拡張したインストールツールで、やはり PyPA によって開発されています。 現在ではデファクトといえるほど使われており、PyPA 自身も「推奨ツール」として紹介しています。

ここまでの登場人物を整理すると、下図のようになります。

Python パッケージングの歴史

パッケージングの標準

ここからいよいよ本題です。

Python には sdist と bdist という2種類のパッケージ配布形式があります。

  • bdist …… Built Distribution: ビルド済み配布物
    • 「置く」だけでインストールできる状態になっている配布形式(ユーザーがソースビルドしなくて良い)
  • sdist …… Source Distribution: ソースコード配布物
    • ソースコードをそのまま zip/tar.gz で固めて配布し、インストール時にビルドしてもらう形式

それぞれについて見ていきましょう。

bdist 形式に関する標準: wheel

distutils 自体は RPM や Windows インストーラーなどのプラットフォームごとの bdist 形式を含んでいました。これに対し setuptools は2004年、独自の egg というフォーマットを策定しました。

setuptools で利用されていたこの形式ですが、PEP もなく、また C API を利用しないライブラリでも Python のバージョンごとにビルドし直す必要があるという問題がありました。 それを解決するために登場したのが wheel です(やはりこれも PyPA 管轄のプロジェクトです)。 この形式は、次のような PEP で egg の問題点を解決しました。

こうして、ライブラリを配布する側は wheel 形式にパッケージを固めて配布するだけ、利用する側はそれをダウンロードして展開するだけで良いという世界観が確立されました。

なお、ファイル形式としての wheel と、それを作るためのライブラリである wheel を混同しないよう注意しましょう。 前者は配布者と利用者が共通して使う「形式(≒プロトコル)」です。 いっぽう後者は配布者がパッケージングのために使うライブラリで、利用者は基本的には使いません。……「基本的に」といったのはもちろん例外があるからなのですが、それについては次節で触れます。

sdist 形式に関する標準: pyproject.toml

sdist 形式のパッケージからライブラリをインストールするには、利用者側が setup.py を実行して bdist 形式のパッケージを作りそれをインストールする、というのがデファクトスタンダードである pip の挙動でした。ここで使われる bdist 形式は、wheel ライブラリがあれば wheel、なければ egg です。

しかし setup.py も1つの Python コードです。 それが何かしらの 3rd party ライブラリに依存していた場合、どうしたらいいでしょうか。 実際、ほとんどの setup.pysetuptools という 3rd party ライブラリに依存しています。

そこでまず決まったのが PEP 453 です。

pip そのものは標準ではないが pip のインストーラーは標準、というなかなか絶妙なさじ加減ですね。

一見良さそうに見えるこのアプローチですが、依然として問題がありました。 それは、setuptools 以外の依存についてはうまく扱えないということです。 このままでは、パッケージングに関する改善に PyPA 以外のコミュニティが貢献できないことにもなります。

こうして登場したのが pyproject.toml です。

いくつか例を見てみます。

例1. 従来方式

このように build-backend を指定しなければ、従来同様 setup.py が実行されます。

1
2
[build-system]
requires = ["setuptools", "wheel"]

なお PEP 518 では、pyproject.toml ファイルがない場合のデフォルト値を上記とすることも定められています。

例2. Poetry

Poetry を使うとこのような例になります。

1
2
3
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

翻訳すると、このパッケージングをインストールするには

  • poetry のバージョン 0.12 以降をインストールして
  • poetry.masonry.api を実行せよ

ということになります。 なお poetry がインストールされるのはビルド用の一時的な環境なので、ユーザー環境は汚染されません。

どのツールを使うかという情報以上のことは、もうあとは標準では関知せず、各ツール任せになります。 たとえばパッケージの依存をどこで定義するか、lock ファイルを利用するかなどは、各ツールが自由に決めます。

Poetry の例では、依存の定義には setup.pysetup.cfg を使わず、pyproject.toml 内の tool.poetry テーブルを利用しています(前述の通り、このテーブルを Poetry が自由に使えることは PEP 518 で定められています)。

1
2
3
4
5
6
[tool.poetry.dependencies]
python = "^3.7"
python-json-logger = "^0.1.11"
[tool.poetry.dev-dependencies]
pytest = "^3.0"

Lock ファイルは poetry.lock を利用しています(当初は pyproject.lock というファイルを利用していましたが、標準で定められていないのに標準かのようなファイル名を使うべきではないということでリネームされました)。

補足: TOML に対する依存について

Python の標準ライブラリには、JSON/ini などとは違い TOML 用ライブラリが含まれていません。

では pyproject.toml パースのための TOML への依存はどうやって管理されているのでしょうか。

これは PEP では定まっておらずインストールツール任せになっています。 実は pippytoml という開発終了したライブラリを使っており、Poetry と同じ tomlkit へ移行することが提案されています

ライブラリ配布者として何をすべきか

ライブラリ配布者としては、sdist 形式に加えて必ず wheel 形式でも配布するようにしましょう。 その方が利用者にとっても使いやすく、配布者側も利用者の pip のバージョンを気にせず pyproject.toml が使えるというメリットがあります。

その理由は pip install が下記のような挙動をするからです。

  1. wheel 形式で配布されている
    • pip が wheel ファイルを展開し配置する
  2. sdist 形式で配布されている(=ビルドが必要)
    • 2-a. pip のバージョンが v19.0 以降、かつ pyproject.toml ファイルがある(または pip--use-pep517 を指定)
      • → PEP517 に従う
    • 2-b. wheel ライブラリが実行環境にある
      • setup.py bdist_wheel を実行して1へ
    • 2-c. それ以外
      • setup.py egg_info && setup.py install

おわりに

以上、Python のパッケージングに関する2019年末時点での標準を整理してみました。 長らく混乱も多いトピックですが、それは先人たちの努力の現れなんですね。 こういった話は明日からすぐ使える知識ではないので疎かにしがちですが、知っておくと Python が向かう未来が見えてくる気がします。

さて、2020年はもうすぐそこです。 まもなく EOL を迎える Python 2 へ感謝をこめて、この記事は終わりにしたいと思います。

参考文献

本記事を書くにあたっては、下記の記事を参考にしました。

日本語ブログ

公式リソース

アイキャッチ画像は WordArt.com によって生成されています。