クラウドインフラ本部ネットワークサービス部のid:a8544です。
前回の記事は「【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように - FJCT Tech blog」でした。
連載4回目の今回は、Pythonのソースコード中のimport文をフォーマットしてくれる、 isort を取り上げます。
Pythonのソースコードフォーマッティング(整形)について、 わたしたちはいくつかのソフトウェアを利用していますが、 そのひとつがisortです。isortを使うことでコード冒頭のインポートに関する悩みが減り、 より本質的なコーディング・レビューへ集中できるようになります。
🤔 インポートとそのお悩みポイント
実用的なPythonプログラムでは、import文はほぼ確実に存在するのではないでしょうか。 Pythonの標準ライブラリ、サードパーティのライブラリを利用する際には、 まずそれらをインポートする必要があります。 また、一連のコードを1つのファイル(モジュール)に全て書くことは現実的でないため、 コードを複数のファイルに分割することもほぼ必ず行われます。 その際も、他のファイルからコードを読み込む際にインポートが必要になります。
Pythonにおけるモジュールのインポートはimport文で行うのが通常です。したがって、 典型的なPythonプログラムの冒頭にはimport文が1つ以上ならぶことになります。
"""サンプル""" import argparse from pathlib import Path def main(): ...
ところで、複数のインポートが存在するとき、どの順で並べて書くべきなのでしょうか。
ある人は、単にインポートが必要なことに気付くたび書き足していって、
import argparse import os from pathlib import Path import sys
…こう書くかもしれません。別の人は、ある程度まとめてコンパクトに
import argparse, os, sys from pathlib import Path
…と書くかもしれません。また、
import argparse import os import sys from pathlib import Path
…と書く人もいるでしょう。
通常の (行儀のよい) モジュールであれば、 読み込む順によって効果が変わることはないはずなので、どうやって書いても基本的には結果は変わりません。
つまり、インポートの書き方にはある程度の自由度があるのです。このこと自体は直ちに問題になるわけではありません。
ただ、ある程度の期間にわたってPythonのコードを書いていると、自分なりの「流儀」が多かれ少なかれ生まれてきます。 そうすると、コードレビューの際に 「自分の流儀と違って気になるなあ、でも指摘するほどのことでもないし…」という思いが生じ、 無駄なモヤモヤを抱えることになります。
このモヤモヤへの対処は単純で、メンバーが合意した「正しい」書き方を決め、 みんながそれにならって書くように徹底すればよいのです。 これがどう実現できるのかを、これから考えていきます。
🐍 PEP 8
まず、そのような書き方を決めるにはどうすればよいのでしょうか。
ここで参考になるのが、 PEP 8のインポートに関する節 です。
それによれば、
import foo, bar
のような単一行での複数のインポートは避けるべき。- ただし from形式では許容される
from somemodule import foo, bar
- ただし from形式では許容される
- インポートはファイルの冒頭、モジュールコメントやdocstringの直後に存在するべき。
- インポートは次の順でグルーピングされるべき。また、グループ間に空行を設けるべき。
- 標準ライブラリ
- サードパーティライブラリのインポート
- そのアプリケーションやライブラリ固有のインポート
といったルールが記述されています (相対インポートの是非やワイルドカードの利用については、 本記事の範囲を超えるため省略します) 。
しかし、PEP 8では同一グループ内での名前の並び順については触れられていません。
また、異なる形式 (import
から始まるものと from
で始まるもの) のインポートを、
同一グループの中でどう整理するか、も触れられていません。このように、PEP 8は
インポート部分の記述方法を一意に定めるまでに詳細な規則ではありません。
import sys from logging import DEBUG, INFO # from形式のインポートを途中にはさんでよい? import os # os -> sys とアルファベット順にすべき?
✨ フォーマッタの利用
そうなると、考慮すべきことがたくさんありそうで、「インポートの正しい書き方」 を決める試みに暗雲が立ち込めてきます。 このままでは、書き方を徹底させる方法を考える前に頓挫してしまいそうです。
ここで元の問題に立ち返ると、達成すべきなのは「インポートの正しい書き方を決めること」 ではなく、「コードレビュー時のモヤモヤをなくすこと」でした。
これを達成するよりよい策は、フォーマッタの利用です。正しい書き方のルールを独自に定めるのではなく、 何かしらのフォーマッタが定義しているルールが正だとするわけですね。さらに、「『正しいルール』に従って書く」と いう行為は単に「フォーマッタにかける」ということになるので、手間がかなり省ける上、 ルールに沿うようにコードを自動修正できるようになります。
isortの利用
以下、 isort 5.12.0 で動作を確認しています。
💿 isortの導入手順
isortの導入は簡単です。PyPI よりインストールするだけです。
Poetry を使っている場合は
$ poetry add isort -G dev Using version ^5.12.0 for isort Updating dependencies Resolving dependencies... (0.1s) Writing lock file Package operations: 1 install, 0 updates, 0 removals • Installing isort (5.12.0) $
とすればよいでしょう。 詳細は公式Webページ をご確認ください。
🚀 isortの起動
$ isort . $
で、ワーキングディレクトリ以下を再帰的にファイルを探索し、修正が必要なら修正してくれます
(Poetryでインストールした場合は、 poetry run
を使います) 。
$ poetry run isort . Fixing /home/a8544/project/isort_sample/bad.py $
他にも使用方法はありますが、わたしたちの場合ほぼこのコマンドしか使っていません。 詳細は公式Webページ をご確認ください。
👶 結果の確認その1 — 簡単な場合
isortによってインポートがどのように変更されるか見てみましょう。
例えば下記のインポートは…
import sys from logging import INFO, DEBUG import os
このように修正されます。
import os import sys from logging import DEBUG, INFO
import
形式→ from
形式の順、さらに同一の形式の中ではアルファベット順、
from
形式の中の列挙でもアルファベット順、というようにソートされています。
別の例も見てみましょう。この例は、単にfrom インポートの名前がアルファベット順でないというだけでなく、 リストが長いために行も長くなってしまっています (作為的ですが…) 。
import os, sys from logging import Logger, CRITICAL, ERROR, WARNING, INFO, DEBUG, getLogger, getLogRecordFactory
これは、isortによって次のように修正されます (設定にもよります) 。
import os import sys from logging import ( CRITICAL, DEBUG, ERROR, INFO, WARNING, Logger, getLogger, getLogRecordFactory )
アルファベット順のソートだけではなく、行が長くなりすぎていることを検知して、適切に折り返しも行ってくれました。 このように、isortは単なるソートのみならず、フォーマットも行ってくれるのです。
🧓 結果の確認その2 — サードパーティライブラリとローカルの区別
PEP 8には、
- 標準ライブラリ
- サードパーティライブラリのインポート
- そのアプリケーションやライブラリ固有のインポート
という順でインポートをグルーピングすることが書かれていました。isortもこれに対応しています。
ただし、そのためには
- isortがプロジェクトのルートを認識できる、あるいは
- ソースコードのパスが明示的に与えられている
必要があるようです。具体例で見てみましょう。
$ tree example/ example/ ├── poetry.lock ├── pyproject.toml └── test_package ├── __init__.py ├── bar │ ├── __init__.py │ └── test.py └── foo └── __init__.py
このようなPoetryプロジェクトを想定します。また、 example/test_package/bar/test.py
の
中身が次のようであったとします。ただし、requestsはサードパーティのライブラリです。
import test_package.foo import requsets import os
この場合、PEP 8にならえば
import os import requsets import test_package.foo
とソートされるべき、ということになります。 test_package
は
ローカル (ファーストパーティ) のライブラリなので、 os
とも requests
とも別のグループになるわけですね。
この通りにisortでソートさせるためには、プロジェクトのルートを認識させる必要があります。 isortは 空ではない 設定ファイルがあれば、そこをルートだと認識してくれるようです。
例えば、
[settings] line_length = 79
という内容の .isort.cfg
ファイルをプロジェクトのルート、
つまり example/.isort.cfg
に作ればよいということです。
ここでは line_length
の設定を指定していますが、
この例のように、何らかの具体的な設定が含まれていないとうまくいかないようです。
pyproject.toml
でプロジェクトの設定を管理している場合は、
.isort.cfg
を作らず、 pyproject.toml
の1セクションとしても記述できます。
[tool.isort] line_length = 79
この場合も内容が空だとローカル (ファーストパーティ) の認識ができないようなので注意しましょう。
明示的にソースコードのパスを指定することでも、ローカル (ファーストパーティ) の認識をさせることができます。
[tool.isort] src_paths = test_package
この場合、当然ながらソースコードが含まれるパスを指定しないと正しく動作しませんので注意しましょう。
🔧 設定の変更
isortのフォーマットの挙動は細かく指定できます。ただし、あまり深入りすると、 また「正しいルール」を決める議論になってしまうかもしれません。 デフォルトで試してみて、気になったところを直す程度に留めるのがよいかと思います。
わたしたちのチームでは、下記の設定で運用しています (pyproject.toml
の記述です) 。
[tool.isort] include_trailing_comma = "True" line_length = 88 multi_line_output = 3 use_parentheses = "True"
include_trailing_comma
かっこによる列挙の最後のアイテムの後にコンマを置くかどうかです。デフォルトだとFalseで、
from test_package.bar import ( lengthy_lengthy_name_1, lengthy_lengthy_name_2, lengthy_lengthy_name_3 )
のようになります。Trueにすると、
from test_package.bar import ( lengthy_lengthy_name_1, lengthy_lengthy_name_2, lengthy_lengthy_name_3, )
となります。
line_length
インポートが長くなった場合、wrapping (折り返し) がなされますが、 その基準となる文字数です。折り返しというのは、例えば
from test_package.bar import lengthy_name____1, lengthy_name____2, lengthy_name____3
は長すぎるので、
from test_package.bar import (lengthy_name____1, lengthy_name____2, lengthy_name____3)
とかっこを使って括った上で、折り返されるといったものです。
行の長さが line_length
を超えた場合、折り返しが実施されます。
デフォルトでは79ですが、少し長くしています。
multi_line_output
fromインポートが長くなった場合、wrapping (折り返し) がなされますが、 その形式を指定します。isortではなんと12個ものモードがあります。 各モードの違いは isortのドキュメントの該当箇所 をご参照ください。
use_parentheses
インポートの1行が長くなったときの折り返しに、バックスラッシュを使うかかっこを使うかの選択です。 Falseだとバックスラッシュを使いますので、
from test_package.foo import \ bar as too_lengthy_lengthy_lengthy_lengthy_lengthy_lengthy_name
となります。Trueだと
from test_package.foo import ( bar as too_lengthy_lengthy_lengthy_lengthy_lengthy_lengthy_name )
となります。
🛑 部分的な無効化
何らかの理由でisortにフォーマットしてほしくない場合、行単位で無効にできます。
from piyo import import_this_frist # isort: skip from baz import then_import_this # isort: skip from foobar import finally_import_this # isort: skip
上記のように、 # isort: skip
を記述することで、その行についてはフォーマットがされなくなります。
🤖 CIでの利用
うまく設定ができたら、CIでも実行するようにしましょう。
CIでは、「isortのルールで正しくフォーマットされているか」を確認します。これは、
isortの --check-only
オプションが便利です。このオプションが与えられると、
実際にファイルは変更せず、インポートが正しくフォーマットされているかどうかだけ確認します。
そして、正しくフォーマットされていれば終了コード0、そうでなければ終了コード1で終了します。
isort --check-only .
さいごに
今回はisortによるインポートのフォーマットについてご紹介しました。
Pythonのインポートは柔軟に書くことができる一方、メンバーによってバラつきが生まれうるところです。 また、行の長さ等の制約に応じてきれいに書くのもなかなか手間で、案外苦労させられます。
isortを導入してしまえば、それ以降インポートについて気を使うことがなくなります。 きれいに整えられたインポートは見るだけでモチベーションが高まる (個人の感想です) ものですので、 Pythonを書く全ての方にぜひともオススメしたいです。
次回も今回に引き続きフォーマッタに関するトピックで、blackを紹介する予定です。お楽しみに。