【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
クラウドインフラ本部ネットワークサービス部の上野(id:uweno)です。 成人女性や男の子でもプリキュアになれる昨今、成人男性の私もそろそろ変身できるんじゃないかなって期待しています。
前回の記事は「【CI戦術編 その3】Pythonのimportのことならまかせろ isort - FJCT Tech blog]でした。 今回は前回に引き続き、PythonのフォーマッタであるBlackについてご紹介します。
統一されたコーディングスタイルの重要性
Pythonでは文法的に同じ内容のコードを複数のスタイルで記述できます。 例えば、以下の4つのリストは、表記方法は違いますが、評価結果は同じです。
list_1 = [1, 3, 4] list_2 = [ 1, 3, 4 ] list_3 = [ 1, 3, 4, ] list_4 = ([1, (3), 4])
これらのリストは同値であるため、どのスタイルを用いても本質的には問題ありませんし、 どのスタイルがより見やすいかは個人の感性の問題であり正解はありません。 しかし、同じリポジトリ内に複数のスタイルが混在している場合、コードを読む際の余計なノイズとなります。 またコードレビューの際にも、スタイルの好みについての議論が集中してしまい、本質的な問題が議論にならない可能性があります。
以上のように、スタイルが統一されていないコードは開発やレビューの足を引っ張る要因になります。 そのためチーム開発をする際には、メンバー間で互いに同意されたコードスタイルを持つことが重要です。
コーディングスタイルに従ったコードの維持
コーディングスタイルについてチーム内で同意できたら、そのスタイルに則ったコードを維持しつつ開発を進めることになります。
スタイル違反がないよう十分に気を付けてコーディングしつつ、コードレビューでもレビュアーが注意深くチェックすれば、スタイル違反の無いコードを保てるかもしれません。
しかしこれはコーディングする側、レビューする側双方にとって大変なコストになりますし、また人力に頼ったチェックではスタイル違反を見逃してしまう可能性もあります。
開発をスムーズに行うために取り入れたコーディングスタイルが開発の足かせになってしまっては本末転倒です。 そこでコーディングスタイル違反を検出し、スタイルに則ったコードへ自動で変換できるフォーマッタを導入することで、 スタイルを遵守したコードの維持を最小限のコストで実現できます。
Blackを用いたコードフォーマット
私たちのチームではBlackというフォーマッタを用いてコードフォーマットを行っています。 BlackはPEP 8準拠で、 PEP 8よりもさらに詳細に定義されたThe Black code styleに則ってコードを整形するフォーマッタです。 PEP 8だけではまだ複数の書き方が可能で一意に定まらない部分を、The Black code styleではがっちりと、ブレがないように定義していています。
Blackはきわめてシンプルな構成になっており、ルールを制御するオプションはほとんどありません。 そのためBlackを導入することは、コーディングスタイルとしてThe Black code styleを採用することと同義です。 Blackを導入することで、自分たちで細かいルールを考えたり議論したりすることなく、The Black code styleに則ったコードへ統一できます。 (自分たちで好みのコードスタイルを考えて、コンフィグを調整しながらコーディングスタイルをつくりあげていきたい場合には向かないツールであるとも言えます)。
poetryを使っている場合、以下のコマンドでBlackをインストールします。
$ poetry add black -G dev
$
以下のコマンドを実行することでPythonのコードをフォーマットできます。
第2引数にはフォーマット対象のファイル、もしくはディレクトリを指定します。
list.py
は、「統一されたコーディングスタイルの重要性」の節で紹介した4種類のリストが書かれたPythonのコードです。
$ poetry run black list.py reformatted list.py All done! ✨ 🍰 ✨ 1 file reformatted, 3 files left unchanged.
コマンドを実行した結果、list.py
は以下のようにフォーマットされました。
list_1 = [1, 3, 4] list_2 = [1, 3, 4] list_3 = [ 1, 3, 4, ] list_4 = [1, (3), 4]
list_1
はBlack code styleに違反しない書き方であるため修正されませんでした。
list_2
は改行が削除されてlist_1
と同じ形に整形されました。
list_3
は要素ごとに改行するリストとして整形されました。
リストの最後にカンマを打つことで、リスト中の各要素が「式+カンマ」で統一され、Gitなど行単位で差分を管理するバージョン管理システムで差分が見やすくなります。
しかし、カンマを入れても1行に複数の要素を列挙してしまうと意味がありません。
そこでBlackでは最後の要素のあとにカンマがあるとき、1行につき1要素になるようにフォーマットします。
list_4
は一番外側に存在した不要なカッコ()
が取り除かれて整形されました。
Blackはネストされたカッコを変更しないため、要素3
を囲む不要なカッコは取り除かれません。
また今回の例の(3)
のカッコは不要なカッコに見えます。
しかし、例えば (True and False) or (False and True)
のように、可読性を高めるために文法上は不要なカッコをつけることはよくあります。
Blackは開発者の意図通り、このようなカッコはそのまま残してくれます。
一方Blackを実行した結果、不自然なカッコが残っていると感じられる場合はカッコのつけすぎかもしれません。
コードを見直すとよいでしょう。
Blackでは1行が長い行の改行、バックスラッシュの削除、空行の削除なども行われます。
def function(hoge: str, fuga: float | None, piyo: int | None, foo: bool, bar: bool, baz: bool) -> None: """1行の長さが88文字を超える場合は改行されます。""" # `\` は削除されます if fuga \ and piyo: print("インデントを開始した直後の空行は削除されます") if ((foo and bar) or (baz and piyo)): print("一番外側のカッコは冗長なため削除されますが、""それより内側のネストされたカッコは削除されません。" \ "このように、コードの見やすさのために""開発者が意図して書いたカッコは残ります。" ) print( "1行の長さが88文字を超える場合でも、文字列のように自動的に改行することができないものはそのまま残ります(この行は多バイト文字も1文字としてカウントした場合105文字あります)", )
以上のPythonファイルに対してBlackを実行した結果、以下のようにフォーマットされます。
def function( hoge: str, fuga: float | None, piyo: int | None, foo: bool, bar: bool, baz: bool ) -> None: """1行の長さが88文字を超える場合は改行されます。""" # `\` は削除されます if fuga and piyo: print("インデントを開始した直後の空行は削除されます") if (foo and bar) or (baz and piyo): print( "一番外側のカッコは冗長なため削除されますが、" "それより内側のネストされたカッコは削除されません。" "このように、コードの見やすさのために" "開発者が意図して書いたカッコは残ります。" ) print( "1行の長さが88文字を超える場合でも、文字列のように自動的に改行することができないものはそのまま残ります(この行は多バイト文字も1文字としてカウントした場合105文字あります)", )
コードフォーマットの部分的な無効化
これまでコーディングスタイルを統一する重要性について説明してきましたが、中には機械的にフォーマットするとかえって読みづらくなるコードもあります。 PEP 8の中でも、「一貫性にこだわりすぎるのは、狭い心の現れである」と指摘されているように、コーディングスタイルを無視する正当な理由がある場合は必ずしもコーディングスタイルに従う必要はありません。
コード中にコメントでfmt: off
、fmt: on
を書くことで、Blackにフォーマットしてほしくない範囲を指定できます。
例えば、以下のように数字の桁数を合わせたいコードでフォーマッタを無効にします。
# fmt: off carrot = 98 potato = 128 beef = 1_980 wine = 12_980 # fmt: on sum = ( carrot*3 ) + ( potato * 5)+ (beef *8 ) + wine
このファイルに対してBlackを走らせた結果、fmt: off
、fmt: on
コメントの間のコードはフォーマットされません。
# fmt: off carrot = 98 potato = 128 beef = 1_980 wine = 12_980 # fmt: on sum = (carrot * 3) + (potato * 5) + (beef * 8) + wine
CIでのBlackの実行
Blackにはフォーマットは行わず、コーディングスタイル違反の有無のみを確認する --check
オプションがあります。
このオプションを指定してBlackを実行すると、コーディングスタイルに適合する場合は終了コード0を返し、コーディングスタイルに違反がある場合は非ゼロのエラーコードを返すため、そのままCIに組み込むことができます。
おわりに
今回はPythonのフォーマッタであるBlackについてご紹介しました。 コーディングスタイルに則った統一されたコードは読みやすくなるため、開発や保守のコストを下げる効果が期待できます。 Blackを導入することで簡単にコーディングスタイルを遵守できるので、Pythonのコーディングスタイルで困っている方はぜひ導入を検討してみてください。 次回はmypyを使った型チェックについてご紹介します。ぜひお楽しみに。
連載バックナンバー
CI戦術編
- 第1回 Alembic Check
- 第2回 cSpell
- 第3回 isort
- 第4回 Black ←イマココ
- 第5回 mypy 3/29更新予定