FJCT Tech blog

富士通クラウドテクノロジーズ公式エンジニアブログです

富士通クラウドテクノロジーズ

FJCT/Tech blog

【CI戦術編 その3】Pythonのimportのことならまかせろ isort

Pythonのimport文を表示しているエディタの画面
Pythonのimport文、どう整理されていますでしょうか?

クラウドインフラ本部ネットワークサービス部のid:a8544です。

前回の記事は「【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように - FJCT Tech blog」でした。

tech.fjct.fujitsu.com

連載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
  • インポートはファイルの冒頭、モジュールコメントやdocstringの直後に存在するべき。
  • インポートは次の順でグルーピングされるべき。また、グループ間に空行を設けるべき。
    1. 標準ライブラリ
    2. サードパーティライブラリのインポート
    3. そのアプリケーションやライブラリ固有のインポート

といったルールが記述されています (相対インポートの是非やワイルドカードの利用については、 本記事の範囲を超えるため省略します) 。

しかし、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には、

  1. 標準ライブラリ
  2. サードパーティライブラリのインポート
  3. そのアプリケーションやライブラリ固有のインポート

という順でインポートをグルーピングすることが書かれていました。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を紹介する予定です。お楽しみに。