朝日ネット 開発者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

2019年に向けてPythonのモダンな開発環境について考える

はじめに

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

techblog.asahi-net.co.jp

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

対象バージョン

前回と同様、この記事でも基本的に 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
    • env 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 の元となったツール)を統合して PipfilePipfile.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 環境はプロジェクト直下に作る
# 処理系はフルパスで実行ファイルを指定してもよい
env 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/activatesource されたシェルが新たに起動します。

pipenv run python        # virtualenv 環境内で python 処理系を起動
pipenv run flask --help  # virtualenv 環境内で flask を起動
pipenv run pytest        # virtualenv 環境内で pytest を起動
pipenv shell             # virtualenv 環境を activate したシェルを起動

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

# 以下のコマンドは別マシン上の Pipfile{,lock} のあるディレクトリ(またはその子孫ディレクトリ)で実行する
# packages がインストールされた virtualenv 環境を用意する
env PIPENV_VENV_IN_PROJECT=true pipenv install
# あるいは packages と dev-pakcages がインストールされた virtualenv 環境を用意する
env PIPENV_VENV_IN_PROJECT=true pipenv install -d

なお、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.pyPipfile で採用されている手法です。

前回の記事で使った python-boilerplate パッケージを例とします。 まず、dev-package として自身 (.) を追加し、setup.pyextras_requiredev として指定していた内容を Pipfile に移動させます 2

env 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]
asahi-py-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 処理系は型ヒントを無視するようになっており、静的な型検査は mypyPyre などの別のツールで行います。

ライブラリの型定義 (*.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
    • PEP 8 (ja) で規定されているコードスタイルのチェッカ
  • 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.setupcmdclass 引数にこれらのクラスを指定します。

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 も現状スタンダードに近い位置にいるというくらいで、数年後にはまた情勢が変わっている可能性もあります。 PoetryFlit のような setuptool のレイヤーまで置き換えようとするツールもあります 8 。 また、プロジェクト設定ファイルとして標準化されている pyproject.toml は複数のツールから使われることをあらかじめ想定しています。 どちらかというと 1 つのツールに依存しすぎない乗り換えのしやすいプロジェクト構成にしておくことを心がけておく方が大事なのかもしれません。

自動コード整形ツールも複数存在していますが、こちらは気軽に乗り換えると git blame の結果が滅茶苦茶になってしまうのでパッケージング関連ツールの場合よりむしろ厄介かもしれません……。

ベストプラクティスはその時々において変わりますし、プロジェクトの性質に応じて異なる設定が必要になってくる場合もあります。 地道に知識をアップデートして定期的にプロジェクト構成について考える機会を設けていきましょう。

採用情報

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


  1. package.json (npm) で言うと packages は dependencies、dev-packages は devDependencies に相当します

  2. これまで通り setup.py で開発者用パッケージも管理し Pipenv は単なる wrapper として使いたい場合は env PIPENV_VENV_IN_PROJECT=true pipenv install -de '.[dev]' とすれば OK です

  3. Python 3.4 でも PyPI から typing ライブラリをインストールすれば型検査は可能です

  4. *.pyi (Stub files) は TypeScript で言うところの *.d.ts であり、typeshed は DefinitelyTyped に相当します

  5. subprocess.call は Python 3.5 以降であれば subprocess.run とすることを推奨します。subprocess.check_call のように異常終了時に例外を投げたい場合はキーワード引数で check=True としてください

  6. API ドキュメント生成については前回記事の「API ドキュメント出力 (sphinx)」の節をご参照ください

  7. npm の npm scripts と似たような仕組み

  8. Poetry の実用例としては Black の pyproject.toml が参考になります