はじめに
開発部の tasaki です。 6 月の記事(「Pythonのパッケージングのベストプラクティスについて考える2018」)では setuptools, pip, venv を使ったパッケージングのフローについて考えました。
今回はモダンな開発用ツールチェーンを持つ他の言語(具体的には JavaScript (Node.js), Go, Rust あたりを意識)と似たような開発フローを Python において構築するにはどうすればよいかということを考えていきます。
- はじめに
- 対象バージョン
- TL;DR (結論)
- pip と virtualenv の統合 (Pipenv)
- 静的な型の検査 (mypy)
- Linting (Flake8)
- 自動コード整形 (autopep8, isort)
- setuptools.Command と Pipenv scripts
- おわりに
- 更新履歴
- 採用情報
対象バージョン
前回と同様、この記事でも基本的に Python 3.4 以降のバージョンを動作対象とします。
備考
- Python 3.4 の end-of-life (EOL) は 2019/03/16 なので、今から新たにプロジェクトを作る場合には 3.4 は無視して構わないかもしれません。
- Python 3.5 については EOL は 2020/09/13 であり Ubuntu 16.04 や Debian 9 の python3 がこのバージョンであるため、広く使われうるプログラムを書く場合はこれを対象とすればよいでしょう。
- Python 3.6 からはフォーマット済み文字列リテラルと dataclasses のバックポートが利用でき利益が大きいため、あえて 3.6 以降のみを対象とすることを検討する余地はあります。Ubuntu 18.04 の python3 は 3.6 であり、Red Hat Enterprise Linux 8 でも 3.6 が python3 パッケージとして標準リポジトリに入る予定です(EPEL にも python36 パッケージが存在します)。
TL;DR (結論)
GitHub に今回作成したプロジェクトを用意しました。
- ikasat/python-boilerplate at v2.0.0 — GitHub
PIPENV_VENV_IN_PROJECT=true pipenv install -d
で開発環境を構築できますpipenv run python-boilerplate
でアプリケーションが起動しますpipenv run vet
で静的な型検査 (mypy) ・Linter (Flake8) が走りますpipenv run fmt
で自動コード整形 (autopep8, isort) が走りますpipenv run doc
で API ドキュメントを生成します (Sphinx)pipenv run build
で wheel パッケージを生成します
pip と virtualenv の統合 (Pipenv)
概要
前回の記事では「venv で仮想環境を作り、仮想環境を activate して pip install
する」という流れでプロジェクトの依存パッケージをインストールしていました。
Pipenv はこの作業をより楽に行えるようにするツールです。
pip と virtualenv(venv の元となったツール)を統合して Pipfile
と Pipfile.lock
ファイルでパッケージのバージョンを管理します。
使い方
詳細なインストール方法と使い方については公式ドキュメントをご参照ください。 この節では操作の基本的な流れのみ説明します。
インストール
Pipenv 自体を pip でインストールします。--user
フラグを付けてユーザディレクトリにインストールすることをおすすめします。
pip3 install --user pipenv
インストールしたツールを使うために PATH
環境変数に ${HOME}/.local/bin
を追加しておきましょう(Linux の場合)。
PATH="${HOME}/.local/bin:${PATH}" export PATH
Pipenv プロジェクトの新規作成
例として、flask を依存パッケージ (packages)、pytest を開発者向け依存パッケージ (dev-packages) とする新規プロジェクトを作成してみます 1 。
# 新規ディレクトリの作成 mkdir new-project cd new-project # Pipfile と virtualenv 環境の新規作成 # 処理系は python3.4 を使用し、virtualenv 環境はプロジェクト直下に作る # 処理系はフルパスで実行ファイルを指定してもよい PIPENV_VENV_IN_PROJECT=true pipenv --python 3.4 pipenv install flask # flask を packages に追加 pipenv install -d pytest # pytest を dev-packages に追加(--dev でも可)
上記のコマンドを実行すると new-project/
下に
.venv
ディレクトリ(flask と pytest がインストールされた virtualenv 環境)Pipfile
ファイル(Pipenv の設定ファイル、TOML 形式)Pipfile.lock
ファイル(Pipenv のロックファイル、JSON 形式)
が生成されます。
Linux の場合、Pipenv はデフォルトでは ~/.local/share/virtualenvs
ディレクトリ下に virtualenv 環境を作成しますが、PIPENV_VENV_IN_PROJECT
環境変数を true
に設定すると Pipfile
と同じディレクトリに .venv
ディレクトリとして virtualenv 環境を作ります。
npm (node_modules/
) のような挙動にしたい場合はこの環境変数を設定しておきましょう。
既に .venv
ディレクトリが存在する場合はそちらを優先して使うため、仮想環境の作成が行われる初回実行以外でこの環境変数を設定する必要はありません。
生成された Pipfile
の内容は以下の通りです。
[[source]] verify_ssl = true url = "https://pypi.org/simple" name = "pypi" [dev-packages] pytest = "*" [packages] flask = "*" [requires] python_version = "3.4"
Pipfile.lock
の方には実際にインストールされたパッケージとその依存パッケージのバージョンが記録されます。
この状態で pipenv run <実行ファイル名>
コマンドを実行すると virtualenv 環境内のアプリケーションを実行できます。
また、pipenv shell
コマンドで .venv/bin/activate
が source
されたシェルが新たに起動します。
pipenv run python # virtualenv 環境内で python 処理系を起動 pipenv run flask --help # virtualenv 環境内で flask を起動 pipenv run pytest # virtualenv 環境内で pytest を起動 pipenv shell # virtualenv 環境を activate したシェルを起動
生成された Pipfile
と Pipfile.lock
を共有すれば他のマシンでもこの環境を再現することができます。
# 以下のコマンドは別マシン上の Pipfile{,lock} のあるディレクトリ(またはその子孫ディレクトリ)で実行する # packages がインストールされた virtualenv 環境を用意する PIPENV_VENV_IN_PROJECT=true pipenv sync # あるいは packages と dev-pakcages がインストールされた virtualenv 環境を用意する PIPENV_VENV_IN_PROJECT=true pipenv sync -d
pipenv sync
を使うことで Pipfile.lock
に指定されたバージョンのライブラリを新しい環境にインストールすることができます。
なお、virtualenv 環境にはどのバージョンの python
処理系と pip
を使うかという情報が含まれている(.venv/bin/python
, .venv/bin/pip
に実行ファイルがシンボリックリンクされる)ことに注意してください。
--python
引数で明示的に処理系のバージョンを指定せずに pipenv install
すると Pipenv は自動で処理系を探します。
Python 処理系のバージョンと Pipfile
でのバージョン指定が異なっていると警告が表示されてしまうので、それが望ましくない場合は [requires]
での python_version
指定を削除またはコメントアウトしておきましょう(TOML 形式なのでコメントアウトは #
で行います)。
[requires] # python_version = "3.4"
setup.py との併用
Pipenv は基本的に pip + virtualenv を統合したツールであり、setuptools のレイヤーを置き換えるツールではありません(重要)。
アプリケーションでなくライブラリを書く場合やパッケージを wheel 形式で配布したい場合など、setuptools の機能が必要な場面では引き続き setup.py
を書く必要があります。
詳しくは公式ドキュメントの以下の節をお読みください。
また、Pipenv は PyPA (Python Packaging Authority) の管理下にあるプロジェクトですが、Pipfile
の仕様等については PEP での標準化はされていません。
Pipenv は現在も活発に更新されている進歩の著しいツールであり、バグのような挙動を見せることも稀にあります。
安定した動作を求める場合、Pipenv は開発時にのみ使い、運用時には従来の setuptools + pip + virtualenv/venv および wheel パッケージを使ったフローを採用した方がよいでしょう。
以下、「実行に必要なパッケージは setup.py
で管理し、開発に必要なパッケージは Pipfile
で管理する」という方法でパッケージを管理する例です。
これは Pipenv 自身の setup.py
と Pipfile
で採用されている手法です。
前回の記事で使った python-boilerplate パッケージを例とします。
まず、dev-package として自身 (.
) を追加し、setup.py
で extras_require
の dev
として指定していた内容を Pipfile
に移動させます 2 。
PIPENV_VENV_IN_PROJECT=true pipenv install -de . # 自身を dev-packages として追加(-e: --editable) pipenv install -d 'pytest>=3' coverage tox sphinx # その他 dev-packages を追加
Pipfile
は以下のようになります。
[packages] [dev-packages] python-boilerplate = {editable = true, path = "."} pytest = ">=3" coverage = "*" tox = "*" sphinx = "*"
開発者は常に pipenv install -d
で開発環境を作成し、その仮想環境を使って開発します。
setup.py
に書かれている実行に必要な依存パッケージ (install_requires
) を変更した場合は pipenv update -d
コマンドを実行すると Pipfile.lock
と virtualenv 環境にインストールされているパッケージを更新できます。
pipenv update -d # あるいは Pipfile.lock の更新と virtualenv 環境の更新を別々に行うこともできる pipenv lock # Pipfile.lock の更新 pipenv sync -d # virtualenv 環境を Pipfile.lock に同期させる
静的な型の検査 (mypy)
概要
Python は動的型付き言語ですが、Python 3.5 から漸進的型付け (gradual typing) のための型ヒント (PEP 484) がサポートされ typing ライブラリが標準で用意されるようになりました 3 。 Python 処理系は型ヒントを無視するようになっており、静的な型検査は mypy や Pyre などの別のツールで行います。
ライブラリの型定義 (*.pyi) は typeshed という Git リポジトリに集められています4。
設定例
デフォルトの状態では型情報のないパッケージを import
すると型検査はエラーとなってしまいます。
現状 Python のほとんどのパッケージに型情報はついていないためこのエラーは抑止した方がよいでしょう。
setup.cfg
(または mypy.ini
)に以下の設定を追記します。
[mypy] ignore_missing_imports = True
使い方
mypy
にディレクトリやファイルをコマンドライン引数として与えて実行すると型検査が行われます。
mypy python_boilerplate
Linting (Flake8)
概要
Flake8 は Python ソースコードのコードスタイルやロジックエラーを静的検査する Linter です。
Flake8 は pycodestyle, pyflakes, mccabe の 3 つの Linter を統一的に扱う wrapper となっています。
- pycodestyle · PyPI
- pyflakes · PyPI
- ロジックエラーのチェッカ
- 未定義の変数・未使用の変数・
import
されているが使われていないパッケージなどをチェックする
- mccabe · PyPI
- McCabe の循環的複雑度 (Wikipedia) を計算するツール
設定例
setup.cfg
(または .flake8
)に以下の設定を追記します。
ここでは Lint 対象から外すファイル・ディレクトリや 1 行あたりの文字数などを記載しています。
[flake8] exclude = .git, .tox, .venv, .eggs, build, dist, docs max-line-length = 120
使い方
引数なしで flake8
を起動するとチェックが行われます。
flake8
自動コード整形 (autopep8, isort)
概要
Flake8 (pycodestyle) で検知したコードスタイル違反を手で 1 つ 1 つ直していくのは骨が折れます。 autopep8 という自動コード整形ツールを使って自動で直してしまいましょう。
また、import
を適切にソートしてくれる isort というツールもあります。
設定例
setup.cfg
の [isort]
セクション(または .isort.cfg
の [settings]
セクション)に以下の設定を追記します。
なお、autopep8 は [flake8]
セクションに書かれた設定を読みに行く(pycodestyle と設定を共有する)ためここでは isort の設定のみ行っています。
[isort] line_length = 120 skip = .git, .tox, .venv, .eggs, build, dist, docs
使い方
整形前後の差分を表示する
isort -df autopep8 -dr python_boilerplate tests setup.py
整形して上書きする
isort -y autopep8 -ir python_boilerplate setup.py tests
備考
autopep8 を含む 4 つのツールの wrapper になっている pyformat というフォーマッタもあります。
autopep8 でのフォーマットに加え未使用 import
の削除、docstring の整形、シングル・ダブルクオートの統一を行えます。
Python 3.6 以降であれば Google 製の YAPF や新進気鋭の Black というツールも使えます。
なお、Black については記事執筆時点で正式リリースされておらず、pip install
/ pipenv install
時にプレリリース版をインストールするための --pre
フラグが必要です。あるいは Pipfile
に以下の指定を行ってください。
[pipenv] allow_prereleases = true
setuptools.Command
と Pipenv scripts
例えば静的チェッカを走らせたい時に mypy python_boilerplate && flake8
、コードを自動フォーマットしたい時に isort -y; autopep8 -ir python_boilerplate setup.py tests
などと毎回入力するのはやや面倒です。
しかし、このようなちょっとした定型コマンドを走らせたい時にシェルスクリプトや Makefile
として書いてしまうと Windows 環境で動作させるのが難しくなってしまいます。
このような時のために setuptools には Command
という仕組みが用意されています。
まず、以下のように setuptools.Command
クラスを継承したクラスを作成し、run
関数をオーバーライドして実行したい処理を書きます 5 。
その他、user_options
フィールドと initialize_options
, finalize_options
関数のオーバーライドが必須です。
オプション引数を取らない場合は全て空にすればよいのですが、毎度書くのは面倒なので SimpleCommand
クラスを作りそれを継承することにします。
import subprocess from setuptools import Command, setup # (中略) class SimpleCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass class VetCommand(SimpleCommand): def run(self): subprocess.check_call(["mypy", PACKAGE_NAME]) subprocess.check_call(["flake8"]) class FmtCommand(SimpleCommand): def run(self): subprocess.call(["isort", "-y"]) subprocess.call(["autopep8", "-ri", PACKAGE_NAME, "tests", "setup.py"]) class DocCommand(SimpleCommand): def run(self): opt = "-f" if os.path.exists(os.path.join("docs", "conf.py")) else "-F" subprocess.call(["sphinx-apidoc", opt, "-o", "docs", PACKAGE_NAME]) if os.name == 'nt': subprocess.call([os.path.join("docs", "make.bat"), "html"]) # for Windows else: subprocess.call(["make", "-C", "docs", "html"])
setuptools.setup
の cmdclass
引数にこれらのクラスを指定します。
setup( ..., cmdclass={ "vet": VetCommand, "fmt": FmtCommand, "doc": DocCommand, }, )
この状態で python setup.py <COMMAND>
と打つと独自に定義したコマンドを実行できます 6 。
python setup.py vet python setup.py fmt python setup.py doc
この状態では virtualenv 環境内にいない時に上記のスクリプトを走らせようとすると pipenv run python setup.py vet
とタイプしなければなりません。
ここで、Pipenv には Pipfile
の [scripts]
で指定したスクリプトを pipenv run <SCRIPT>
の形で実行できる機能があります 7 。
Pipfile
に以下の内容を追記します。
[scripts] doc = "python setup.py doc" fmt = "python setup.py fmt" vet = "python setup.py vet" build = "python setup.py bdist_wheel"
こうすることで指定のコマンドを virtualenv 環境の中で実行することができます。
pipenv run vet pipenv run fmt pipenv run doc pipenv run build
おわりに
今回は各種ツールを利用して Python の開発環境を構築するための現時点でのベストプラクティスについて考えてみました。
Pipenv は「人間のための Python 開発ワークフロー」という公式の説明の通り開発を楽にするためのツールであり、何か劇的に新しいことができるようになるというものではありません。
実際のところ、依存パッケージをそれぞれのパッケージ毎に管理するだけであれば pip と venv さえあればほぼ同じことができますし、ロックファイルも pip freeze
を駆使すればそれなりに実現できます。
しかし、そのフローは Python 世界においては統一されておらず、依存関係管理・バージョン管理の方法もパッケージ毎にまちまちでした。
Pipenv の価値はこれを「とにかく pipenv install -d
すれば開発環境を用意でき、pipenv run ...
すればアプリケーションが動く」という流れにしてくれるところにあると思います。
とはいえ、pip + venv を統合するツールは複数存在し、Pipenv も現状スタンダードに近い位置にいるというくらいで、数年後にはまた情勢が変わっている可能性もあります。
Poetry や Flit のような setuptool のレイヤーまで置き換えようとするツールもあります 8 。
また、プロジェクト設定ファイルとして標準化されている pyproject.toml
は複数のツールから使われることをあらかじめ想定しています。
どちらかというと 1 つのツールに依存しすぎない乗り換えのしやすいプロジェクト構成にしておくことを心がけておく方が大事なのかもしれません。
自動コード整形ツールも複数存在していますが、こちらは気軽に乗り換えると git blame
の結果が滅茶苦茶になってしまうのでパッケージング関連ツールの場合よりむしろ厄介かもしれません……。
ベストプラクティスはその時々において変わりますし、プロジェクトの性質に応じて異なる設定が必要になってくる場合もあります。 地道に知識をアップデートして定期的にプロジェクト構成について考える機会を設けていきましょう。
更新履歴
- 2019/04/05
- 他のマシンで環境を再現する際に
pipenv install
ではなくpipenv sync
を使うよう変更
- 他のマシンで環境を再現する際に
- 2019/11/19
- 初版
採用情報
朝日ネットでは新卒採用・キャリア採用を行っております。
-
package.json
(npm) で言うと packages は dependencies、dev-packages は devDependencies に相当します↩ -
これまで通り
setup.py
で開発者用パッケージも管理し Pipenv は単なる wrapper として使いたい場合はPIPENV_VENV_IN_PROJECT=true pipenv install -de '.[dev]'
とすれば OK です↩ -
Python 3.4 でも PyPI から typing ライブラリをインストールすれば型検査は可能です↩
-
*.pyi (Stub files) は TypeScript で言うところの *.d.ts であり、typeshed は DefinitelyTyped に相当します↩
-
subprocess.call
は Python 3.5 以降であればsubprocess.run
とすることを推奨します。subprocess.check_call
のように異常終了時に例外を投げたい場合はキーワード引数でcheck=True
としてください↩ -
API ドキュメント生成については前回記事の「API ドキュメント出力 (sphinx)」の節をご参照ください↩
-
npm の npm scripts と似たような仕組み↩
-
Poetry の実用例としては Black の
pyproject.toml
が参考になります↩