これは Python Advent Calendar 2015 の 1 日目の記事です。
Wheel というのは、最近になって導入された Python の新しいパッケージング規格です。 今回はその Wheel を pip が積極的にキャッシュすることで気づいた点について書いてみます。
今回使用した環境は次の通りです。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.11.1 BuildVersion: 15B42 $ pip list | egrep (pip|setuptools|wheel) pip (7.1.2) setuptools (18.5) wheel (0.26.0)
気づけば Wheel を使っていた
Python を使っている方であれば、パッケージマネージャの pip は使ったことがあるとおもいます。 その pip ですが、実は最近のバージョンでは知らず知らずのうちに裏側で Wheel を使うような仕組みになっていることをご存知でしょうか。 実は pip を使って PyPI のパッケージをインストールしようとしたときには、必ず Wheel を使ってインストールするようになっています。
PyPI に Wheel があるとき
PyPI にはたくさんのパッケージが登録されていますが、その中には Wheel が用意されているものといないものがあります。 これはパッケージを作って登録したひとがソースコード配布物とは別に Wheel パッケージもアップロードするか否かによって変わります。 まずは Wheel が用意されている場合ですが、これはその Wheel がインストールにそのまま使われます。
試しに PyPI に Wheel が用意されているパッケージの代表として requests をインストールしてみましょう。
$ pip install requests Collecting requests Downloading requests-2.8.1-py2.py3-none-any.whl (497kB) 100% |████████████████████████████████| 499kB 545kB/s Installing collected packages: requests Successfully installed requests-2.8.1 $ pip list | grep requests requests (2.8.1)
ログから Wheel (*.whl) がダウンロードされてインストールに使われたことがわかります。
PyPI に Wheel がないとき
PyPI に Wheel がなくてソースコード配布物しか用意されていない場合はどうでしょうか。 実は、その場合にも内部的には Wheel が使われるようになっています。
これがどういうことなのか確認するため、Wheel が用意されていないパッケージの代表として lxml をインストールしてみましょう。 この作業には lxml に含まれる C 拡張モジュールのコンパイルが必要なため時間がかかります。 time コマンドを使ってインストールに要した時間も記録しておきましょう。
$ time pip install lxml Collecting lxml Downloading lxml-3.4.4.tar.gz (3.5MB) 100% |████████████████████████████████| 3.5MB 138kB/s Building wheels for collected packages: lxml Running setup.py bdist_wheel for lxml Stored in directory: /Users/amedama/Library/Caches/pip/wheels/62/29/26/11383932dbed36e9fe68552fc755a96cfc6fa5833d948620da Successfully built lxml Installing collected packages: lxml Successfully installed lxml-3.4.4 pip install lxml 46.51s user 1.84s system 94% cpu 51.298 total
インストールには 46 秒かかりました。
ダウンロードされたのはソースコード配布物 (*.tar.gz) ですが、"Building wheels for collected packages: lxml" というログからもわかる通り、それを Wheel にビルドした上でインストールしていることがわかります。 そして、"Stored in directory: " となっているところは今回の本題となる Wheel のキャッシュです。
つまり、最近の pip は PyPI に Wheel が用意されていない場合、ソースコード配布物を Wheel にビルドした上でインストールし、更にそれをキャッシュするようになっているということです。
Wheel の何がうれしいのか?
Wheel の利点は色々ありますが、一番分かりやすいのは C 拡張モジュールがコンパイル済みの状態で同梱されているところだとおもいます。
試しに、先ほどインストールした lxml を一旦アンインストールした上で、もう一度インストールしてみましょう。 今度もインストールの際には time コマンドを使って要した時間を記録します。
$ pip uninstall -y lxml Uninstalling lxml-3.4.4: Successfully uninstalled lxml-3.4.4 $ time pip install lxml Collecting lxml Installing collected packages: lxml Successfully installed lxml-3.4.4 pip install lxml 0.71s user 0.11s system 98% cpu 0.825 total
なんと今度は 1 秒かからずにインストールが終わっています。 これはキャッシュされた Wheel を使ってインストールが行われたため、パッケージのダウンロードと C 拡張モジュールをコンパイルする時間がいらなくなったためです。
Wheel キャッシュにひそむ罠
このようにインストールにかかる時間の短縮に絶大な効果を発揮する Wheel のキャッシュですが、その反面思わぬ事態を招くこともあることに最近気付きました。
試しに pudb というパッケージをインストールしてみましょう。 これは Python 向けのグラフィカルなデバッガです。 このパッケージには、まだ PyPI に Wheel が用意されていないためソースコード配布物を手元でビルドしてそれをローカルにキャッシュすることになります。
$ pip install pudb==2015.4.1 Collecting pudb Downloading pudb-2015.4.1.tar.gz (49kB) 100% |████████████████████████████████| 53kB 2.3MB/s Collecting urwid>=1.1.1 (from pudb) Downloading urwid-1.3.1.tar.gz (588kB) 100% |████████████████████████████████| 589kB 635kB/s Collecting pygments>=1.0 (from pudb) Downloading Pygments-2.0.2-py3-none-any.whl (672kB) 100% |████████████████████████████████| 675kB 666kB/s Building wheels for collected packages: pudb, urwid Running setup.py bdist_wheel for pudb Stored in directory: /Users/amedama/Library/Caches/pip/wheels/f5/42/17/0cb57e3de0702f797b1f19a1dcbe84b04d2b9ca3e750d75176 Running setup.py bdist_wheel for urwid Stored in directory: /Users/amedama/Library/Caches/pip/wheels/20/ed/aa/f28c05890e0dd72e7023a9fb3e5966a0a9d7a1e9682a29a94f Successfully built pudb urwid Installing collected packages: urwid, pygments, pudb Successfully installed pudb-2015.4.1 pygments-2.0.2 urwid-1.3.1
pudb のインストールが終わると pudb3 というコマンドが使えるようになります。
$ pudb3 Usage: pudb3 [options] SCRIPT-TO-RUN [SCRIPT-ARGUMENTS] Options: -h, --help show this help message and exit -s, --steal-output --pre-run=COMMAND Run command before each program run
この末尾についている 3 という数字は pudb 自体のバージョンではなく、インストール先の Python バージョンによって付与されるようになっています。
つまり、使っているのが 3.x 系であれば末尾に '3' が付与されて、2.x 系であればつかないという寸法です。 今回は使用したバージョンが 3.4 だったのでつきました。
$ python --version Python 3.4.3
それでは、今度は Python 2.7 の仮想環境を作りなおした上で、改めて pudb をインストールしてみましょう。 これには前述した通り、先ほどビルドした Wheel のキャッシュが使われます。 なんだか嫌な予感がしてきたでしょうか? 尚、仮想環境の作成手順については省略しています。
$ python --version Python 2.7.10 $ pip install pudb==2015.4.1 Collecting pudb Collecting urwid>=1.1.1 (from pudb) Using cached urwid-1.3.1.tar.gz Collecting pygments>=1.0 (from pudb) Downloading Pygments-2.0.2-py2-none-any.whl (672kB) 100% |████████████████████████████████| 675kB 530kB/s Building wheels for collected packages: urwid Running setup.py bdist_wheel for urwid Stored in directory: /Users/amedama/Library/Caches/pip/wheels/20/ed/aa/f28c05890e0dd72e7023a9fb3e5966a0a9d7a1e9682a29a94f Successfully built urwid Installing collected packages: urwid, pygments, pudb Successfully installed pudb-2015.4.1 pygments-2.0.2 urwid-1.3.1
インストール先の Python のバージョンが 2.7 なのだから、コマンド名に 3 はつかないはずです。 しかし、そうはなりません。 なぜか 3 が末尾に付与されたコマンドがインストールされています。
$ pudb zsh: command not found: pudb $ pudb3 Usage: pudb3 [options] SCRIPT-TO-RUN [SCRIPT-ARGUMENTS] Options: -h, --help show this help message and exit -s, --steal-output --pre-run=COMMAND Run command before each program run
一体、何が起こったのか?
ひと言にいえば、ビルドした環境の Python バージョンによってコマンド名が pudb3 に固定された状態でその Wheel がキャッシュされてしまったせいです。 誤解を避けるためにあらかじめ断っておきますが、これは Wheel というシステムの問題ではなく設定の不備によって引き起こされています。 適切に Wheel の設定が行われている限りこの問題は起きません。
それでは、詳しく見ていきましょう。 Wheel では、パッケージの作り方によっては Python のバージョン 2.x と 3.x に共通で使用できる単一の Wheel ファイル (*.whl) を作ることもできます。 これは、setup.cfg の wheel セクションに universal フラグを 1 にすることでそれを示すことになっています。 そして先ほどインストールした pudb (バージョン 2015.4.1) についてもそのようになっています。
しかし、前述した通り pudb はインストール先の Python のバージョン毎にコマンド名を変更するようになっています。 そして、コマンド名は Wheel ファイルをビルドするタイミングで決まってしまいます。 つまり、厳密にいえばひとつの Wheel ファイルをふたつのメジャーバージョンで使い回すことはできないわけです。
試しに手元で pudb のソースコードをチェックアウトして手動で Wheel ファイルをビルドしてみましょう。 できあがった Wheel のファイル名には py2.py3 というバージョン番号が入っている通り、どちらのバージョンでも使えることを示しています。
$ git clone https://github.com/inducer/pudb.git $ cd pudb $ git checkout -b v2015.4.1 v2015.4.1 $ python setup.py bdist_wheel $ ls dist pudb-2015.4.1-py2.py3-none-any.whl
しかし、実際には universal = 0 とすることで、Python 2.x と 3.x で別々に Wheel ファイルをビルドする必要があったわけです。
$ sed -i -e "s:^\(universal = \)1$:\10:" setup.cfg $ cat setup.cfg [flake8] ignore = E126,E127,E128,E123,E226,E241,E242,E265,W503,E402 max-line-length=85 [wheel] universal = 0 $ python setup.py bdist_wheel $ ls dist | grep py2- pudb-2015.4.1-py2-none-any.whl
設定に不備があるパッケージを扱うには?
ここまで見てきたように、設定の不備があるパッケージはたまにあるようです。 pip が積極的に Wheel ファイルをキャッシュするようになったのは最近のことなので、まだまだ問題に気づかれていないのだとおもいます。
では、設定に不備があるパッケージをそれでもインストールしなければならないときにはどうすればいいのでしょうか。 実は pip には --no-cache-dir というオプションがあります。 これを使えば Wheel ファイルをキャッシュせずにインストールできます。
$ pip install --no-cache-dir pudb
既にキャッシュされてしまった Wheel を削除したいときには、以下のコマンドで Wheel がキャッシュされている場所を調べましょう。
$ find $(python -c "from pip.locations import USER_CACHE_DIR; print(USER_CACHE_DIR)") -name "*.whl" /Users/amedama/Library/Caches/pip/wheels/20/ed/aa/f28c05890e0dd72e7023a9fb3e5966a0a9d7a1e9682a29a94f/urwid-1.3.1-cp27-none-macosx_10_11_x86_64.whl /Users/amedama/Library/Caches/pip/wheels/20/ed/aa/f28c05890e0dd72e7023a9fb3e5966a0a9d7a1e9682a29a94f/urwid-1.3.1-cp34-cp34m-macosx_10_11_x86_64.whl /Users/amedama/Library/Caches/pip/wheels/62/29/26/11383932dbed36e9fe68552fc755a96cfc6fa5833d948620da/lxml-3.4.4-cp34-cp34m-macosx_10_11_x86_64.whl /Users/amedama/Library/Caches/pip/wheels/f5/42/17/0cb57e3de0702f797b1f19a1dcbe84b04d2b9ca3e750d75176/pudb-2015.4.1-py2.py3-none-any.whl
まとめ
今回は、最近の pip が新しいパッケージング規格である Wheel をこっそりと裏側で使っていることを示しました。 その上で、Wheel をビルドする際の設定に不備があると、まずい状態の Wheel ファイルがキャッシュされてしまい、ハマる原因になることを解説しました。 パッケージを作る際には、setup.cfg の中にある wheel セクションの universal フラグを慎重に設定する必要があります。
補足
今回の件については pudb にプルリクエストを送って既にマージされたため、次のリリースでは直っているはずです。 github.com
おまけ
今回の件で pip のソースコードをちょこっと読みました。
以下は pip がキャッシュを取得するメカニズムです。 PyPI のソースコード配布物を示す URL から生成された Link オブジェクトが、WheelCache#cached_wheel() メソッドを通すとローカルのキャッシュを指し示すものに変換されています。
>>> from pip import wheel >>> from pip import index >>> from pip import locations >>> >>> url = 'https://pypi.python.org/packages/source/p/pudb/pudb-2015.4.1.tar.gz#md5=2589255f1885a9eab10f666ca0f6204c' >>> link = index.Link(url) >>> cache = wheel.WheelCache(locations.USER_CACHE_DIR, index.FormatControl(set(), set())) >>> cache.cached_wheel(link, 'pudb') <Link file:///Users/amedama/Library/Caches/pip/wheels/f5/42/17/0cb57e3de0702f797b1f19a1dcbe84b04d2b9ca3e750d75176/pudb-2015.4.1-py2.py3-none-any.whl>
また、これもソースコードを読んでわかったのですが Wheel キャッシュに使われる長いディレクトリ名は URL を SHA224 でハッシュしたものになっているようです。
>>> url = 'https://pypi.python.org/packages/source/p/pudb/pudb-2015.4.1.tar.gz#md5=2589255f1885a9eab10f666ca0f6204c' >>> import hashlib >>> hashlib.sha224(url.encode()).hexdigest() 'f542170cb57e3de0702f797b1f19a1dcbe84b04d2b9ca3e750d75176'
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る