はじめに

開発部の tasaki です。 6 月の記事（「Pythonのパッケージングのベストプラクティスについて考える2018」）では setuptools, pip, venv を使ったパッケージングのフローについて考えました。

techblog.asahi-net.co.jp

今回はモダンな開発用ツールチェーンを持つ他の言語（具体的には JavaScript (Node.js), Go, Rust あたりを意識）と似たような開発フローを Python において構築するにはどうすればよいかということを考えていきます。

対象バージョン

前回と同様、この記事でも基本的に Python 3.4 以降のバージョンを動作対象とします。

備考

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 PIPENV_VENV_IN_PROJECT = true pipenv --python 3 . 4 pipenv install flask pipenv install -d pytest

上記のコマンドを実行すると new-project/ 下に

.venv ディレクトリ（flask と pytest がインストールされた virtualenv 環境）

ディレクトリ（flask と pytest がインストールされた virtualenv 環境） Pipfile ファイル（Pipenv の設定ファイル、TOML 形式）

ファイル（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 pipenv run flask --help pipenv run pytest pipenv shell

生成された Pipfile と Pipfile.lock を共有すれば他のマシンでもこの環境を再現することができます。

PIPENV_VENV_IN_PROJECT = true pipenv sync 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 . pipenv install -d ' pytest>=3 ' coverage tox sphinx

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 pipenv lock pipenv sync -d

静的な型の検査 (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 となっています。

設定例

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

例えば静的チェッカを走らせたい時に 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" ]) 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 初版



採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。