FJCT Tech blog

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

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

FJCT/Tech blog

【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた

【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた

ネットワークサービス部の上野(id:uweno)です。

今回はpyupgradeを使って、Pythonの記法を最新にする方法についてご紹介します。

前回の記事は【CI戦術編 その6】Python開発の強い味方 Pylint - FJCT Tech blogでした。

tech.fjct.fujitsu.com

Pythonの処理系のバージョンアップ

長年Pythonで開発していると、開発期間中にPythonのバージョンを上げることがよくあります。 例えば、私たちのプロダクトはPython 3.8系で開発が始まりましたが、2023年4月現在はPython 3.11系で開発しています。 Pythonの処理系をバージョンアップすることで、新機能が利用可能になったり、処理速度が向上したりするなどのメリットがあります。 そのため、最新の安定版に追従したいと思うのは当然です。

Pythonのバージョンアップに伴い、新しい記法が使えるようになることがあります。 例えば、以前はlistやset、dictなどの組み込みのコンテナ型をジェネリクスとして利用できなかったため、 型アノテーションを書くにはtypingをimportする必要がありました。

from typing import List, Set, Dict

list_var: List[int] = [1,2,3]
set_var: Set[int] = {1, 2, 3}
dict_var: Dict[int, str] = {1: 'apple', 2: 'lemon'}

Python3.9からはPEP 585が実装され、 組み込みのコンテナ型でもジェネリクスが利用できるようになり、typingのimportは不要になりました。

list_var: list[int] = [1,2,3]
set_var: set[int] = {1, 2, 3}
dict_var: dict[int, str] = {1: 'apple', 2: 'lemon'}

新しい記法が使えるようになったとはいえ、既存の記法が使えなくなるわけではありません (コードの互換性を維持するため、バージョンアップで破壊的な変更が行われることはまれです)。

ただし、新しい記法が実装されたということは、既存の記法に不満を持っていた人が一定数いたことを意味し、 新しい記法ではその不満点が改善されています。

Pythonのバージョンアップを実施したのであれば、新しい記法は積極的に活用していきたいですね。

pyupgradeを用いた新記法への対応

Pythonをバージョンアップしたので、さっそく新記法を使ってみましょう!

と意気込んでも、既存のコードを全て確認し、手動で新しい記法に直すのは大変です。 また新しくコードを書く際にも、これまでの手癖で古い書き方をしてしまうかもしれません。

CI編第5回の記事では、Blackを使ってコーディング規約に則りコードを統一する方法を紹介しました。 「できるだけ新しい記法に準拠する」という内容をコーディング規約に含めることが望ましいですが、 残念ながらBlackではそのような規約を追加する方法は提供されていません。

pyupgradeを使用することで、古い記法から新しい記法にコードを修正できます。

Poetryを使っている場合、以下のコマンドでpyupgradeをインストールします。

$ poetry add pyupgrade --group dev
$

次に、以下のファイルに対してpyupgradeを実行してみましょう。

# file.py
from typing import Optional, Set, Union, Sequence

list_var: Sequence[int] = [1, 2, 3]
set_var: Set[int] = set([5, 6, 7])

int_or_str: Union[int, str] = 1
int_or_none: Optional[int] = 5

Python 3.11の記法に合わせるためには、--py311-plus オプションを指定します。

$ poetry run pyupgrade --py311-plus file.py
Rewriting file.py
# file.py
from typing import Optional, Set, Union
from collections.abc import Sequence

list_var: Sequence[int] = [1, 2, 3]
set_var: set[int] = {5, 6, 7}

int_or_str: int | str
int_or_none: int | None = 5

修正された点は以下の通りです。

  • PEP 585に対応する修正

前述のように、組み込みのコンテナ型でジェネリクスが利用できるようになりました。 typingからインポートされたクラス(Set)は、組み込みのコンテナ型(set)に統一されます。 なお、不要になったimportは自動で削除されないため、注意が必要です。 またコレクションの抽象基底クラスはcollections.abcに定義され、 Python 3.9移行ではtypingのコレクションの使用は非推奨になりました(現状、collections.abcのエイリアスです)。 したがって、Sequenceのインポート元はcollections.abcに統一されます。

UnionとOptionalを用いた型の表記が、パイプ(|)を用いた表記に統一されます。

pyupgradeを用いたより強力なコードの統一

pyupgradeはその名の通り、Pythonの記法をバージョンアップするためのツールなのですが、 バージョンアップとは直接関係ない部分でもフォーマッタとして機能します。

例えば、集合の定義はset([x, y, ...])としても{x, y, ...}としても同様の結果が得られますし、 また、辞書を内包表記を使って定義する際、dict(...) を経由する必要はありません。

CI編第5回で紹介したBlackでは、どちらの記法に統一することはなく、開発者が個性を出す余地がありました。

set_var = set([5, 6, 7])
dict_var = dict([(i, i) for i in set_var])

pyupgradeは、不要な場合にset()dict()を取り除き、{...}の表記に統一します。 上記のファイルに対してpyupgradeを実行すると、以下のように修正されます。

set_var = {5, 6, 7}
dict_var = {i: i for i in set_var}

pyupgradeとBlackでは修正する対象が大きく異なり、 両方を併用してもコンフリクトすることはありません(少なくとも私たちのプロダクトでは発生していません)。 pyupgradeとBlackを併用することで、 Blackを単体で使うよりもよりも強力なフォーマットが可能で、ブレのないコードを維持できます。

CI上でのpyupgradeの実行

私たちは以下のコマンドを実行するジョブをCIに組み込んでいます。

$ find project/path/ -name "*.py" -print0 | xargs -0 poetry run pyupgrade --py311-plus
$

pyupgradeはディレクトリを引数に取れないため、findで検索したファイルのパスを展開してpyupgradeの引数に渡しています。 pyupgradeはpre-commitで使うことを意識して作られており、今回のような使い方は作成者の想定外なのかもしれません。

新しい記法への書き換えが可能な場合、pyupgradeはファイルを書き換えた上で非ゼロの終了コードを返します。 この連載で何度も繰り返している通り、非ゼロの終了コードが出力されるとCIのジョブは失敗となります。

終わりに

今回はpyupgradeを用いて、Pythonの記法を最新にする方法についてご紹介しました。 pyupgradeを使うことで、Pythonの最新の表記に追従でき、 レビュー時に古い記法について指摘される心配がなくなります。 今までの習慣からつい古い記法を使ってしまう経験の多いエンジニアにとって、 より有用なツールとなるでしょう。

次回はOASの自動生成についてご紹介します。お楽しみに。

連載バックナンバー