Python Monthly Topics

新しい静的コード解析ツール「Ruff」ご紹介

福田@JunyaFffです。今月の「Python Monthly Topics」は、最近私が個人的に気になっている静的コード解析ツールRuffについて紹介します。

どんなプログラミング言語でも、静的コード解析ツール(リンター)やフォーマッターは非常に便利です。Pythonでコードを書く場合、皆さんはどんなツールを使っているでしょうか?Flake8やBlack、isortなどが人気で、世界中で多くのPythonエンジニアに利用されています。

Ruffは2022年8月にリリースされた比較的新しい、Pythonのリンター兼フォーマッターです。Ruffはリリースからまだ半年足らずしか経っておりませんが、多くの著名なライブラリで採用[1]され、毎日のようにアップデートされています。2023年3月時点でのRuffの使い方、そしてこれからの発展について、本記事で紹介します。

Ruffとは?

ここではRuffのコンセプトや特徴を紹介します。

Ruffのコンセプト

Ruffは以下の2つの仮説に基づいて作られています。

  1. Pythonのツールは、より処理性能の高い言語に書き換えられる
  2. 統合されたツールは、バラバラのツールセットでは得られない効率を得られる

たとえば、JavaScriptの場合、関連するツールがいろいろな言語で書かれています(esbuildがGo、SWCやRomeがRust、BunがZigなど⁠⁠。Pythonも同じ発想で関連するツールを別の言語で実装すれば高速化できるのでは、というのが1つ目の仮説です。

2つ目の仮説は、現在Pythonで静的コード解析やフォーマットに使われるツールが複数あり、それぞれが構文解析をしているため、もっと効率よくできるのでは?という点です。たとえば、Blackでコードフォーマットをし、isortでimportの順序を修正した後にFlake8で静的コード解析をすることがあります(Flake8はさらに3つの静的コード解析ツールを1つにまとめたものです⁠⁠。それぞれのツールがそれぞれソースコードをチェックしています。Ruffは1回の処理で目立ったパフォーマンスの低下なく、すべてのチェック・自動修正が行われることを目標の1つにしています。

これらの仮説[2]から、RuffはRustで作られており、また複数のツールにインスパイアされたさまざまなルールが実装されています。現時点では、静的コード解析のツールとして、そして次のステップとして本格的なフォーマッターの機能を拡張していく、というフェーズのようです[3]

Ruffの特徴

Ruffの特徴として、速度とルールについて説明します。

速度

2月のPython Monthly Topicsで紹介されたPolarsと同じく、RuffもRustで実装されており、高速です。

Ruffの速度比較
引用元CPythonに対しリンターを実行した速度( https://beta.ruff.rs/docs/ )。ベンチマークの計測について、公式ドキュメントにも記載があります( Benchmarks - Ruff)。

またパフォーマンスの低下を防ぐため、実行結果をキャッシュしています。速度については簡易的に計測した結果を後述します。

静的コード解析のルール

RuffはFlake8やFlake8のプラグイン、その他多くのリンターにインスパイアされた400以上の静的コード解析のルールが実装されています。デフォルトでは、Flake8に含まれているPyflakes、pycodestyleのルールでチェックします。

  • Pyflakes: 不具合の元になりそうな箇所はないかチェック(エラーコードがFで始まるもの)
  • pycodestyle: PEP 8に準拠しているかチェック(エラーコードがE、Wで始まるもの)

他にもさまざまなルールが実装されており、オプションで利用できます。ルールには以下のようなものがあります。さまざまなリンターで提供されているルールと同じチェックが可能です。

  • McCabe:循環的複雑度によるコードの複雑さ
  • isort:importの順序
  • pep8-naming:PEP 8の命名規則
  • pydocstyle:docstringのフォーマット
  • flake8-bugbear:バグになりそうなコードの検出

ここではすべて紹介できませんので、詳細は公式サイトをご確認ください。

なおRuffは、現時点ではフォーマッターであるBlackと一緒に使用するように設計されています。そのため、Blackのようなフォーマッターを使用した後にチェックが不要なルールの実装は先送りされているようです[4]

Ruffのインストール

動作確認に使用したPython、Ruffのバージョンは以下のとおりです。RuffはPython 3.7以上をサポートしています。

  • Python 3.11.1
  • Ruff 0.0.252

Ruffはpipコマンドで簡単にインストールできます。

$ pip install ruff

インストールすると、コマンドラインでruffコマンドを実行できます。

$ ruff --version
ruff 0.0.252

Ruffによる静的コード解析とオプション

まずは簡単なコードに静的コード解析を実行して、出力結果を確認してみましょう。 デフォルトの設定で出力される問題点をコメントで記載しています。

リスト test.py
import os, sys, io  # 問題点1:1行に複数のインポート 問題点2:ioをインポートしているが未使用


def greetingPath(msg, name):  # 命名規則にエラー。後述のオプションで説明
    print(sys.argv)
    path = os.get_exec_path()  # 問題点3:path変数が未使用
    print(f"{msg}, {name}")

このコードに対して、ruffコマンドを実行します。チェックするためのコマンドは ruff check ですが、 check は省略可能です。デフォルトでチェックされるルールは、Flake8に含まれているPyflakes、pycodestyleのルールになります。

E401F841というエラーコードと問題点が出力されます。Flake8と同様の出力がされるため、Flake8をご利用の方には見慣れたフォーマットかと思います。

$ ruff test.py
test.py:1:1: E401 Multiple imports on one line
test.py:1:17: F401 [*] `io` imported but unused
test.py:6:5: F841 [*] Local variable `path` is assigned to but never used
Found 3 errors.
[*] 2 potentially fixable with the --fix option.

問題点を解決するには、ファイルを修正し再度チェックをします。

また別の方法として、出力結果の末尾に [*] 2 potentially fixable with the --fix option.とあるように、Ruffのコードフォーマット機能--fixで自動修正も可能です。 コードフォーマットについては後述します。

オプション

Ruffではオプションを利用することで柔軟にルールを指定できます。主なオプションは次のとおりです。すべてのオプションは公式サイトをご確認ください。

オプション 概要
select 指定したルールをチェックの対象とする
ignore 指定したルールをチェックの対象としない
fixable 指定したルールを修正の対象とする
unfixable 指定したルールを修正の対象としない
exclude ファイルやディレクトリを除外する
line-length 1行の最大文字数を変更する(デフォルトは88)

オプションはpyproject.tomlruff.toml、コマンドラインで指定できます。

pyproject.toml でオプションを指定するサンプルは以下のとおりです。Rules - Ruff にある、ルール(たとえばPyflakesのルール全体を対象とする場合はF、一部を指定する場合はF401を指定します。

例として、以下のようにselectignoreにルールを設定し、先ほどのコードを再度チェックします。出力結果が異なることを確認してみましょう。

リスト pyproject.toml
[tool.ruff]
select = ["I", "N"]  # "I"はisort, "N"はpep8-naming
ignore = ["E", "F"]  # "E"はpycodestyleのError, "F"はPyflakes

この状態で再度ruffコマンドを実行すると、selectに追加したINのエラーが出力され、ignoreに追加したEFで始まるエラーがなくなります。

$ ruff test.py
test.py:1:1: I001 [*] Import block is un-sorted or un-formatted
test.py:4:5: N802 Function name `greetingPath` should be lowercase
Found 2 errors.
[*] 1 potentially fixable with the --fix option.

Ruffのコードフォーマット

Ruffは--fixによってコードフォーマットができます。

$ ruff --fix

フォーマットの対象となるのは、selectオプションで指定しているルールです。

fixableオプションでは、selectオプションで指定しているルールのうち、どれを修正対象とするか指定できます。fixableオプションのデフォルトは、すべてのルールが指定されているため、selectで指定したルールがすべて修正対象となります。

先ほどの、 test.py の最初の出力結果をおさらいしてみましょう。

出力されているエラーのうち、F401 [*] `io` imported but unusedF841 [*] Local variable `path` is assigned to but never usedがRuffの--fixで自動的に修正できるエラーです。Ruffで検出されたエラーがすべて修正できるわけではなく、修正できるのは一部ですpyproject.tomlでのオプションは設定していない状態を想定しています⁠⁠。

リスト test.py - 修正対象のコード
import os, sys, io


def greeting_path(msg, name):
    print(sys.argv)
    path = os.get_exec_path()
    print(f"{msg}, {name}")
リスト チェック時のエラー出力
$ ruff test.py
test.py:1:1: E401 Multiple imports on one line
test.py:1:17: F401 [*] `io` imported but unused
test.py:6:5: F841 [*] Local variable `path` is assigned to but never used
Found 3 errors.
[*] 2 potentially fixable with the --fix option.

--fixでフォーマットを実行します。

3つのエラーが見つかり、2つが修正され、1つが残ることがわかります。

リスト チェックをした時のエラー表示
$ ruff --fix test.py
ruff --fix test.py
test.py:1:1: E401 Multiple imports on one line
Found 3 errors (2 fixed, 1 remaining).

修正後のコードは次のようになります。残っているエラーは1行に複数のimportを記載しているPEP 8違反のエラーになります。

リスト test.py - 修正後のコード
import os, sys


def greeting_path(msg, name):
    print(sys.argv)
    os.get_exec_path()
    print(f"{msg}, {name}")

残っているエラーのE401 Multiple imports on one lineを修正するには、isortのルールである、Iをチェック対象に追加します。前述のpyproject.tomlのselectオプションに"I"を追加を追加し、Ruffの実行を試してみてください。

コードフォーマットで気になった点

利用した所感として、普段使い慣れているBlackやisortでは修正されないエラーをRuffでは修正してしまう点が、個人的には少し気になりました。気になったのは次の2点です[5]

  • 未使用のインポートの削除 - test.py:1:17: F401 [*] `io` imported but unused
  • 未使用変数の削除 - test.py:6:5: F841 [*] Local variable `path` is assigned to but never used

修正をしたくない場合には、設定ファイルにて次のように unfixable を設定することで回避できます。

リスト pyproject.toml
[tool.ruff]
select = ["E", "F", "I"]  # "I" はisort
unfixable = ["F401", "F841"]

そのため、Ruffのフォーマットを利用する場合、以下の方針を検討する必要がありそうです[6]

  • 個人で利用する場合
    • Ruffに任せてみる
    • 修正箇所を確認してから実行する
  • プロジェクトで利用する場合にはメンバーの合意をとる

Ruffでのフォーマットについては、⁠現時点ではベストエフォートな修正」とFAQ[7]に記載があります。また、⁠フォーマットが強いと感じる場合、 unfixable オプションを利用しルールやカテゴリを除外するように」ともありました。

コードフォーマット機能については次のような議論がされており、今後のさらなる改善が期待されます。

Ruffの実行速度について

続いて、Ruffの速度についてみてみましょう。比較はFlake8と行います。計測の対象は手元にあった古いプロジェクトです。ファイル数はおよそ4000ファイル、コードの行数は70万行、指摘の件数は2万件程度になります。

Ruffではまだ実装途中のルール[8]があります。そのため、同等のチェックの結果になるように検出された結果をもとに調整後、 time コマンドで計測を行いました。

$ time ruff .
$ time flake8 .

Flake8に比べて処理にかかった時間が10分の1以下になっています。対象の規模が大きければ大きいほど、処理速度の差を体感できます。

項目 結果:Flake8 結果:Ruff
処理にかかった時間(real) 11.9s 0.7s
ユーザーCPU時間(user) 18.1s 2.2s
システムCPU時間(sys) 2.3s 1.1s

小さなファイルでも計測してみると違いがわかります。ぜひ簡単なファイルで試してみてください。

そのほかの機能

さまざまな実行方法

IDEへの対応

RuffはVisual Studio CodeなどのIDEに対応しています。

vscodeでのRuff

詳細については以下をご確認ください。

--watch機能

--watchでファイルの変更を監視できます。

以下のようにディレクトリやファイルを指定し、--watchでruffコマンドを実行します。ファイルに変更がある場合に自動的にチェックされ、その結果が出力されます。

リスト --watchによるファイル変更の監視
$ ruff . --watch
# ファイルの変更を検知しチェック
[03:06:25 PM] File change detected...

# エラーを出力
[03:06:25 PM] Found 2 errors. Watching for file changes.

test.py:1:1: E401 Multiple imports on one line
test.py:1:1: I001 [*] Import block is un-sorted or un-formatted

# ファイルの変更を検知しチェック
[03:06:42 PM] File change detected...

# エラーがなくなったことを出力
[03:06:42 PM] Found 0 errors. Watching for file changes.
...

pre-commitやtox

gitでcommitする際にさまざまなツールを実行するpre-commitや、テストや静的コード解析などをまとめて実行するtoxでの利用も可能です。

リスト .pre-commit-hooks.yaml サンプル
- repo: https://github.com/charliermarsh/ruff-pre-commit
  rev: 'v0.0.252'
  hooks:
    - id: ruff

flake8からの移行

Ruffの作者の方がFlake8からの移行ツールを公開しています。Flake8の設定を読み込み、Ruff用の設定ファイルを出力するコマンドラインツールです。

リスト flake8-to-ruff
$ pip install flake8-to-ruff
$ flake8-to-ruff path/to/.flake8

詳細については以下をご確認ください。

チェックの例外を設定する - # noqa

RuffではFlake8と同様にnoqaを利用し、行単位、ファイル単位でルールをチェック対象から除外できます。

リスト noqaによる行単位の除外
x = 1  # noqa: F841
リスト noqaによるファイル単位の除外
# ruff: noqa
import os
...

またRuffではこのnoqaを管理する機能があります。

  • 独自ルールRUF100を指定すると、未使用のnoqa(関係ないルールが指定されてる)をチェック・削除できる
  • --add-noqaによって、エラー行に対し、自動でnoqaを付与できる

既存プロジェクトにて、未使用のnoqaの削除は便利な機能です。詳細については以下を確認してください。

まとめ

Ruffのコンセプトである速度と1つのツールでの利便性は大きなメリットです。また、既存の有名なライブラリでの採用もRuffを選択する後押しとなるでしょう。まずはリンターとして、ぜひ試してみてください。私も少人数のプロジェクトでリンターとして利用を始めました。そこで得られた知見などまた別の機会に共有できればと思います。

Ruffの開発のスピードはとても早く日々アップデートされています[9]。今後のアップデートによって、フォーマッターとしても活躍が期待できます。

また本記事では、現在広く使われているFlake8やBlackの詳細な説明はしませんでした。Ruffと併せて気になる方は、公式ドキュメントや、『Pythonエンジニア育成推進協会監修 Python実践レシピ』|技術評論社をご参考いただけると幸いです(PEP 8やFlake8、Blackについて、私が担当しました⁠⁠。

Ruffには詳細なコントリビューションガイドがあります。Rustに興味があるPythonエンジニアのみなさま、この機会にコントリビュートをモチベーションにしても良いのではないでしょうか。

本記事を掲載しているgihyo.jpでは、Rustの連載もあるようです。こちらも併せてチェック!

おすすめ記事

記事・ニュース一覧