エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Pythonのパッケージ周りのベストプラクティスを理解する

Dutch Panzerhaubitz fires in Afghanistan

砲撃する自走砲(PzH2000自走榴弾砲)。自走砲は戦車によく似ていますが、戦車ではありません。*本編とは関係ありません。

こんにちは、エムスリー基盤開発チーム小本です。

Pythonのパッケージ管理周りでは、

setup.pyrequirements.txtを読み込むのが普通なんですよね?」
pipenv があれば venv はオワコンなんですね?」
pyenvは要らないんですよね!?」
「Pythonは歴史が古い分、Rubyなどに比べてカオス」

みたいな混乱をよく目にします。

実際、複数のツールがあって(一見)複雑です。また「なぜこうした状況にあるのか」がドキュメント化されているわけでもありません。

なので、私なりに整理してみることにしました。

「追伸」を追加しました。この記事では汎用プログラミング言語としてPythonを使うケース(Webアプリとか、CLIツールとか、ライブラリを開発するケース)を想定しています。データ分析用にPythonを使っている方は「追伸」も合わせてご覧ください。

TL;DR

  1. 基本的には pipenvを使えばOK
    • pipenv が他ツールのラッパーになっている
    • pip と venv も、直接使う機会は少ないが知っておくべき
    • LinuxやMac なら pyenv, direnvも使うと便利
  2. プロジェクトにはとりあえず以下の4ファイルを含める
    • setup.py
    • setup.cfg
    • Pipfile
    • Pipfile.lock
    • requirements.txt はレガシーなので他ファイルに移行する
  3. 「依存パッケージのバージョン」は、プロジェクトによって書くべきファイルが異なる
    • 私のプロジェクトはライブラリだ → setup.cfg
    • 私のプロジェクトはアプリケーションだ → Pipfile と Pipfile.lock
  4. (追伸)「使い捨て」「データ分析用」「ファイルが1個しかない」などなら、そもそも依存パッケージ管理は不要(かもしれない)

目次

そもそもどんな問題があったのか?

なぜ、こんな状況になっているかを理解するには「そもそもどんな問題があって、こんなツールやファイルが作られたのか?」を振り返らなくてはなりません。 経緯が分かれば、なぜ pip と pipenv を使い分けるのか、理解しやすくなるはずです。

※以下はあくまで「説明用のお話」です。正しい歴史的な記述ではありません。

setup.py

初期のPythonにはパッケージ管理ツールは無く、pythonコマンドがあるだけでした。しかし、それでは開発規模が大きくなってくると色々な問題が起きてきます。

まず、パッケージをインストールする方法がバラバラなのが問題となりました。

大抵は .py ファイルを site-packages/ にコピーすればよいのですが、パッケージによってインストールすべきファイルが異なります。また、拡張ライブラリではインストール前にコンパイルをしなければなりません。これもパッケージによって手順がまちまちでした。

そこで setup.py の仕組み(distutils / distribute / setuptools)が作られました。

パッケージを公開するときに以下のような setup.py スクリプトも含めます。 インストールは python setup.py installコマンドに統一されました。

from setuptools import setup, find_packages

setup(
    name='hoge',
    version='1.0.0',
    description='A sample Python project',
    packages=find_packages(exclude=['contrib', 'docs', 'tests']),

    # 他にもパラメータが色々
)

pip install

次に、パッケージのダウンロードが問題となりました。setup.py を実行する前に、パッケージのファイル自体をダウンロードしなければなりませんが、パッケージの数が増えると大変になってきます。

そこで、pip コマンドが作られました。

pip install hogeで自動でPyPIからhogeをダウンロード&ZIPファイルを展開しsetup.pyを実行してくれます。

setup.py の install_requires

次に、パッケージの依存パッケージのインストールが問題となりました。パッケージが進化すると「hoge のx.y.zも同時にインストールしてください」といった前提条件があるものが出てきました。数が増えてくると人力で把握するのは困難です。

そこで、setup.pysetup()関数に install_requires 引数が追加されました。パッケージの作者は install_requires に依存パッケージの名前とバージョンを書くことができます。すると、pip install は依存パッケージも自動でインストールしてくれます。

setup.cfg

次にsetup.py の複雑さが問題になりました。

setup.py はあくまでPythonスクリプトなので、他のツールから読み込むのは簡単ではありません。 また、setup()の呼び出し以外の処理をする「行儀の悪い setup.py」を書けてしまいます。

そこで、setup.cfg が追加されました。setup()のパラメータは代わりに setup.cfg に書けるようになりました。setup.cfgはiniファイルライクな形式なので解析が簡単です。

requirements.txt

次に、インストールしたパッケージ群を、正確にもう一度インストールし直す方法が問題になりました。

正確にインストールできなければ、

  • 開発環境と本番環境でバージョンが違う
  • 君のPCと僕のPCでバージョンが違う

といったことが起き、不具合につながるからです。

そこで requirements.txt というファイル形式が pip に追加されました。requirements.txtはこんな風に、パッケージ名とバージョンを列挙したファイルです:

ansible==2.2.0
apachelog==1.0
appnope==0.1.0
asn1crypto==0.24.0
astroid==1.3.6
awscli==1.11.33
backports-abc==0.5

...

requirements.txtは以下のように pip で作成・インストールできます。

ある環境のパッケージ一覧を保存
$ pip freeze > requirements.txt

別の環境で復元(パッケージをインストール)
$ pip install -r requirements.txt

devel-requirements.txt

次に、開発環境と本番環境の区別が問題になりました。例えば、ユニットテスト用のpytestは開発時には使いますが、本番では不要です。

そこで、requirements.txt を複数個作ることが考案されました。 開発用のパッケージはdevel-requirements.txtに書けば良いのです。

本番環境
$ pip install -r requirements.txt

開発環境
$ pip install -r requirements.txt -r devel-requirements.txt

venv (virtualenv)

さて、パッケージをインストールできるようになりましたが、

別のプロジェクトで使うパッケージが混ざってしまう問題が起きました。実際の仕事では、複数のプロジェクトを並行して開発するのはよくあることです。しかし、パッケージのインストール先は site-packages/ 1つだけなので、プロジェクトごとに使うパッケージのバージョンが違う時には困ります。

そこで venv が作られました。venv はPythonの「仮想環境」を作ります。つまり、プロジェクトごとに独自の site-packages/ を持てるのです。

なおvenvはPython 3.2 まではvirtualenvという名前でした。

pipenv

次にパッケージのアップデートが問題になりました。

requirements.txtパッケージ名==バージョンを全て列挙しただけのものなので、アップデート作業が大変でした

  • そもそも行数が多くて読みにくい
  • 「直接使っているパッケージ」と「パッケージが依存しているだけで、直接は使わないパッケージ」の区別がつかない
  • 以下の2つが区別できない
    • 絶対にそのバージョンでなければならない(新しいバージョンにはバグがあるなど)
    • たまたまそのバージョンを使っているだけで、バージョンを上げてもよい

といった問題がありました。

そこで pipenv と、requirements.txtに代わるPipfileとPipfile.lockとが、作られました。

Pipfileには「直接使うパッケージ」だけを書きます。また、バージョンは更新できないパッケージだけに指定します。

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
"pandas" = "*"
"cx_Oracle" = "== 5.3" # サーバー環境の関係上、更新できないパッケージ

[dev-packages]
yapf = "*"
mypy = "*"
pytest = "*"

一方 Pipfile.lock には全パッケージの正確なバージョンが書かれます。なお、Pipfile.lock は自動更新され、通常は内容を見る必要すらありません。

これにより、正確なインストールをしつつ、アップデートもできるようになりました。

pyenv

次に、複数のPython自体をビルドするのが大変という問題が起きました。

requirements.txt や pipenv で依存パッケージを、本番環境と開発環境で正確に一致させられるようになりました。しかし、使用するPythonのバージョンは手動管理のままでした。障害を防ぐには、Pythonのバージョンも一致させるべきです。

しかし、複数プロジェクトで開発していると、プロジェクトごとにPythonのバージョンも異なるので、各バージョンのPythonを自分でビルドしなければなりませんが、これも面倒な作業です。

そこで、pyenvが作られました。pyenv install x.y.zで、ソースをダウンロードしてビルドしてくれます。

pipenvその2

次に開発環境を作る手順が面倒という問題が起きました。

開発を始めるには、

  • pyenv でPythonをビルドする
  • venv を仮想環境を作る
  • pip install でパッケージをインストールする

という、複数のステップを踏まなければなりません。

pipenv にはこれらを自動で実行する機能があり、

$ pipenv install

を実行すると、pyenvでPythonのビルド, venvで仮想環境の作成を自動で実行してくれます。

direnv

次にプロジェクトごとに仮想環境を切り替えるのが面倒という問題が起きました。

仮想環境を使えるようにするには、所定のコマンドを実行しなければなりません。

$ . path/to/env/bin/activate

しかし、1日にいくつものプロジェクトを行き来しながら開発する場合は、面倒です。

そこで direnv が注目されました。direnv はディレクトリ を移動するごとに、環境変数を切り替えたり、コマンドを実行したりできるツールです。

最新版のdirenvには layout pipenv コマンドがあります。

layout pipenv

なお pyenv にもディレクトリごとにPythonを切り替える機能があるのですが、direnv の方が汎用的なので、direnv の方がベターでしょう。

補足(古いツールについて)

実際には、各ツールは試行錯誤しつつ発達したので、ツール間に機能の重複があったりします。例えば、setup.py には開発用の依存パッケージを指定する extra_requires というパラメータがあったり、pyenv には仮想環境を管理するプラグインがあったりします。

また、上の説明では、現在では使われていない古いツールは省いています。

古いツール 後継ツール
easy_install pip install
virtualenv-wrapper pipenv
pythonz pyenv
pythonbrew pyenv

他言語との比較

こうして、長々と書くとPythonってやっぱり複雑なのねと思うかもしれません。

しかし、パッケージ周りの問題はPython固有ではありません。他言語でも起きることです。例えば、Pythonと何かと比べられるRubyには、下表のようなPythonの対応物が存在しています。

Ruby Python
*.gemspec setup.py, setup.cfg
gem pip
bundle pipenv
rbenv pyenv
Gemfile Pipfile
Gemfile.lock Pipfile.lock
(対応物なし?) venv

Goも最近depという公式ツールが追加されました。

JavaはMavenなどにはPipfile.lock相当のファイルが無いようですが、それで本番・開発で差異が生じて苦労するケースをたまに見かけます(「Jacksonが古いのバージョンのままやんけー!」的な)。

だから、声を大にして言わせてくださいPythonが特別複雑なわけではない!他の言語と同じだ!と。混乱しているように見えるのは、Pythonではなく、パッケージ管理の複雑さ理解していないあなたの方だ。と。

とはいえ、Bundlerが2010年に登場したRubyに比べ、Pythonはpipenv 登場が2018年なので暗黒期が長かったのは否めませんが、過去の話です。

プロジェクトに含めるべきファイル

以下の4ファイルで管理します。

  • setup.py
  • setup.cfg
  • Pipfile
  • Pipfile.lock

setup.py は以下のような定型文でOKです。むしろ余計な処理を加えない方が望ましいでしょう。

from setuptools import setup
setup()

パッケージ名やバージョンなどのメタ情報(かつて setup 関数で指定したもの)は、setup.cfg に書きます。詳しくは setuptoolsのドキュメント を参照してください。

[metadata]
name = my_package
version = attr: src.VERSION
description = My package description

[options]
install_requires =
  requests
  importlib; python_version == "2.6"

PipfileとPipfile.lock は pipenv install で自動生成します。

使用するPythonのバージョンを指定します。
$ pipenv install --python=3.7.1

site-packages/ に現在のパッケージのシンボリックリンクを作る。`python setup.py develop` に相当する
$ pipenv install -e .

依存パッケージを追加する
$ pipenv install sqlalchemy
$ pipenv install --dev yapf

なお Pipfile はエディタで編集しても良いファイルです(というか編集せざるを得ない)。一方、Pipfile.lock は編集禁止です。こちらは pipenv が自動更新してくれるからです。

ところで、依存パッケージを指定する場所が、setup.cfg の install_requires と、Pipfile/Pipfile.lock の2つあるのにお気付きでしょうか?どこに依存パッケージを書くかはプロジェクトの種類によって異なります

パッケージ管理に使うツール

以下の3コマンドを使えばOKです。

実際に使うのは、ほぼ pipenv のみです。pipenv が pip, venv のラッパーになっているからです。

MacやLinuxなら以下のツールも使うと便利です(Windowsの人は涙を飲んでください)。

また、pipenv, pyenv, direnv は、Pythonに同梱されないので自分でインストールします。インストール方法は各々のリンク先を参照してください。

依存パッケージはどのファイルに書く?

Pythonプロジェクト、大きく2種類に分けられます。

  • ライブラリ
    • 他のライブラリやアプリケーションに組み込まれて使うもの(PyPIで公開するのはほぼライブラリ)
  • アプリケーション
    • 直接使うことができるもの。他のプロジェクトに組み込まれることは基本的に無い

そして、依存パッケージをどこに書くかも種類によって異なり、結局以下のようになります。

  • ライブラリ
    • 依存パッケージは setup.cfgのinstall_requires に書く
    • このとき依存パッケージのバージョンは緩く指定する
    • Pipfile/Pipfile.lock には開発用の依存パッケージだけを指定する
  • アプリケーション
    • setup.cfgのinstall_requires には何も指定しない
    • Pipfile/Pipfile.lock には本番環境用の依存パッケージと、開発用の依存パッケージの両方を指定する(本番は[packages] に、開発は[dev-packages]に書く)

なぜ、こうするべきかというと「プロジェクトの『依存パッケージ』」が異なるものを指しているからです

ライブラリの「依存パッケージ」は、パッケージの依存パッケージのインストールで問題になっているところの「依存パッケージ」です。ですから、install_requires で指定します。

一方、アプリケーションの「依存パッケージ」は、インストールしたパッケージを、正確にもう一度インストールする方法で問題になっている「インストールしたパッケージ」のことです。アプリケーションは(普通は)他プロジェクトにインストールしたりしないので、アプリケーションの依存パッケージをinstall_requires で指定するのはおかしいのです。

なお、このあたりは、pipenvのドキュメント(Pipfile vs setup.py)や、他のブログ記事にも書かれています(私もpipenvとかBundler以前に知っておいて欲しいことを書いた)。

requirements.txt をどうするか?

requirements.txt はレガシーなので、他のファイルに中身を移しましょう。どのファイルに移すかはパッケージの利用目的によります。

アプリケーションが本番環境で使うパッケージは Pipfileに移します。pipenv install -rを使えばOKです。

$ pipenv install -r requirements.txt # 中身を Pipfile の `[packages]`にコピー
$ rm requirements.txt # いらないので削除する

開発用のパッケージも Pipfile に移します。

$ pipenv install --dev -r requirements.txt  # 中身を Pipfile の `[dev-packages]`にコピー
$ rm requirements.txt # いらないので削除する

なお、pipenv install -rPipfileにコピーしたパッケージの中には、バージョン指定が不要なもの、直接は使わないものがあるかもしれないので、手動で修正します。

ライブラリの依存パッケージが requirements.txtに書かれている — requirements.txt が setup.py の中で読み込まれて、install_requires の値として使われている — という場合は、setup.cfg の install_requires に移します。

[options]
install_requires =
  requests
  sqlalchemy

Python 2系での注意点

2019年1月現在はPython 3.7 が最新ですが、いまだに時々Python 2.7 を見かけます(特にOSがデフォルトで使う Python)。

Python 2.7 であっても、パッケージ周りは基本的に同じです。 pipenvやpyenvなどのツールは Python 2.7にも対応しています。 また、pyenvでビルドすれば、新しいバージョンのPythonを使って開発ができるので、OSのPythonが古かったとしても、あまり問題にはなりません。

とはいえ、2.7 は過去のバージョンなので、2.7では動かなかったり、バグが放置されているツールもあります。

また、2.7 では venv が標準添付されておらず、代わりに virtualenv をインストールする必要があります。

追伸:「こんな複雑なことやってられないよ!」問題について

はてなブックマークコメント他では「複雑すぎる」「Anacondaで十分」という反応がありました。これは、ある意味正しいご意見です。

Python言語自体を勉強しているときや、アドホックなデータ分析など、1回しか実行しないケースについては pipenv などは大げさです。pip や Anacondaで「直接依存する」パッケージをインストールすれば十分でしょう。

一方で、

  • 同じスクリプトを繰り返し実行する
  • 複数の環境で実行する(「本番 vs 開発」「僕のPC vs 君のPC」)

といった条件下では「そもそもどんな問題があったのか?」で説明したような問題が発生します。

PythonをWEB開発・CLIツール・ライブラリの開発に使うとき(この記事では暗黙のうちにこのケース想定しています)はもちろん、 データ分析でも複数人で共有したりスクリプトをバッチ的に使ったりするようになると、pipenv や pyenv が必要になるはずです。

追伸:この記事の有効期限について

この記事の情報は2019年1月時点のものです。数年後にはベストな方法ではなくなっているでしょう。・・・とは言え、過度に怖がる必要もありません。

新しいツールが出たからと言って、古いツールがすぐに使用不能になることはありません。歴史あるプロジェクトの中では今だに requirements.txt をコミットしているものがたくさんあります。

まとめ

  1. 基本的には pipenvを使えばOK
    • pipenv が他ツールのラッパーになっている
    • pip と venv も、直接使う機会は少ないが知っておくべき
    • LinuxやMac なら pyenv, direnvも使うと便利
  2. プロジェクトにはとりあえず以下の4ファイルを含める
    • setup.py
    • setup.cfg
    • Pipfile
    • Pipfile.lock
    • requirements.txt はレガシーなので他ファイルに移行する
  3. 「依存パッケージのバージョン」は、プロジェクトによって書くべきファイルが異なる
    • 私のプロジェクトはライブラリだ → setup.cfg
    • 私のプロジェクトはアプリケーションだ → Pipfile と Pipfile.lock
  4. (追伸)「使い捨て」「データ分析用」「ファイルが1個しかない」などなら、そもそも依存パッケージ管理は不要(かもしれない)

エンジニアを募集しています!

エムスリーでは、顧客向けレポートの作成や、機械学習の分野でPythonを使っています。

一緒に働く仲間を募集中です。お気軽にお問い合わせください。

jobs.m3.com