McCabe を使って Python コードの複雑度をチェックしよう!

記事タイトルとURLをコピーする

McCabe とは

github.com

Python コードの複雑さをチェックしてくれるモジュールです。複雑さをチェックすることで、保守性の悪いコードをリリース前に検知することが可能です。もちろん、コードレビューで検知することも出来ますが、複雑と感じるかは個人差がありますので、予めチーム内で指標を決めて機械的にチェックする方が生産性が高そうです。
尚、複雑さ は具体的には 循環的複雑度 と呼ばれる指標を用います。

循環的複雑度とは

循環的複雑度(英: Cyclomatic complexity)とは、ソフトウェア測定法の一種である。Thomas McCabe が開発したもので、プログラムの複雑度を測るのに使われる。プログラムのソースコードから、線形的に独立した経路の数を直接数える。

循環的複雑度 - Wikipedia

具体的な定義としては以下の通りです。

M = E − N + 2P

M = 循環的複雑度
E = グラフのエッジ数
N = グラフのノード数
P = 連結されたコンポーネントの数

はい、よく分かりませんね。試しに適当なコードで計測してみると以下の結果となりました。

# mccabe_sample.py  
  
# 循環的複雑度: 1
def foo(name):
    print(f"Hello {name}!")
  
# 循環的複雑度: 2
def bar(flg):
    if flg:
        print("True")
    else:
        print("False")
  
# 循環的複雑度: 5
def fizzbuzz():
    for i in range(1, 100+1):
        if i % 3 == 0 and i % 5 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)
  
if __name__ == "__main__":
    foo("Taro")
    bar(True)
    fizzbuzz()

ざっくり、if 文やら for 文が増えると複雑度が増す、と考えておけば良さそうです。
また、一般的には以下の通り、10以下が望ましいようです。

循環的複雑度 複雑さの状態 バグ混入確率
10以下 非常に良い構造 25%
30以上 構造的なリスクあり 40%
50以上 テスト不可能 70%
75以上 いかなる変更も誤修正を生む 98%

引用元: 循環的複雑度 - MATLAB & Simulink

試してみる

実際のプロジェクトへの導入方法を確認します。

インストール

pip でインストールが可能です。

$ pip install mccabe

正しくインストールされていれば以下コマンドでヘルプを確認可能です。

$ python -m mccabe -h
Usage: mccabe.py [options]

Options:
  -h, --help            show this help message and exit
  -d, --dot             output a graphviz dot file
  -m THRESHOLD, --min=THRESHOLD
                        minimum complexity for output

実行方法

引数にチェック対象のファイルを指定します。

$ python -m mccabe mccabe_sample.py
2:0: 'foo' 1
5:0: 'bar' 2
11:0: 'fizzbuzz' 5

-m オプションで指定した複雑度以上のもののみ出力します。

$ python -m mccabe -m 5 mccabe_sample.py
11:0: 'fizzbuzz' 5

-d オプションで Graphviz 形式で出力することが出来ます。

$ python -m mccabe -d mccabe_sample.py
graph {
subgraph {
node [shape=circle,label="2:0: 'foo'"] 4316136976;
node [shape=circle,label="Stmt 3"] 4316137168;
4316136976 -- 4316137168;
}
subgraph {
node [shape=circle,label="5:0: 'bar'"] 4316137312;
node [shape=circle,label="If 6"] 4316138464;
node [shape=circle,label="Stmt 7"] 4316138272;
node [shape=circle,label="Stmt 9"] 4316138176;
node [shape=point,label=""] 4316138368;
4316137312 -- 4316138464;
4316138464 -- 4316138272;
4316138464 -- 4316138176;
4316138272 -- 4316138368;
4316138176 -- 4316138368;
}
subgraph {
node [shape=circle,label="11:0: 'fizzbuzz'"] 4316137984;
node [shape=circle,label="Loop 12"] 4316137840;
node [shape=circle,label="If 13"] 4316137696;
node [shape=circle,label="Stmt 14"] 4316137504;
node [shape=circle,label="If 15"] 4316137408;
node [shape=circle,label="Stmt 16"] 4316136352;
node [shape=circle,label="If 17"] 4316136256;
node [shape=circle,label="Stmt 18"] 4316136064;
node [shape=circle,label="Stmt 20"] 4316135968;
node [shape=point,label=""] 4316136160;
node [shape=point,label=""] 4316136448;
node [shape=point,label=""] 4316137600;
node [shape=point,label=""] 4316137744;
4316137984 -- 4316137840;
4316137840 -- 4316137696;
4316137840 -- 4316137744;
4316137696 -- 4316137504;
4316137696 -- 4316137408;
4316137504 -- 4316137600;
4316137408 -- 4316136352;
4316137408 -- 4316136256;
4316136352 -- 4316136448;
4316136256 -- 4316136064;
4316136256 -- 4316135968;
4316136064 -- 4316136160;
4316135968 -- 4316136160;
4316136160 -- 4316136448;
4316136448 -- 4316137600;
4316137600 -- 4316137744;
}
subgraph {
node [shape=circle,label="If 23"] 4316135632;
node [shape=circle,label="Stmt 24"] 4316135488;
node [shape=circle,label="Stmt 25"] 4316128784;
node [shape=circle,label="Stmt 26"] 4316128880;
node [shape=point,label=""] 4316135536;
4316135632 -- 4316135488;
4316135632 -- 4316135536;
4316135488 -- 4316128784;
4316128784 -- 4316128880;
4316128880 -- 4316135536;
}
}

可視化してみると以下の結果が得られました。丸の中の数字はファイル内の行数です。初見の複雑なコードリーディングにも役立つかもしれないですね。

Graphviz で可視化

flake8 のプラグインとして

McCabe は flake8 にデフォルトで同梱されています。その為、既に flake8 を導入済みのプロジェクトでは McCabe を追加でインストールする必要はありません。
以下のコマンドで mccabe が表示されていることを確認しておきましょう。

$ flake8 --version
5.0.4 (mccabe: 0.7.0, pycodestyle: 2.9.1, pyflakes: 2.5.0) CPython 3.9.13 on Darwin

実行方法は以下の通りです。McCabe 単体での実行ではファイル指定が必要で、ディレクトリでの指定は出来ないのですが、flake8 のプラグインとしての実行の場合はディレクトリ指定(以下の例では . でカレントディレクトリを指定)が可能です。こちらの方が実用的ですね。

$ flake8 --max-complexity 4 .
./mccabe_sample.py:11:1: C901 'fizzbuzz' is too complex (5)

注意点としてはオプションの --max-complexity についてです。単体で実行する場合の -m オプションに相当するものですが、複雑度 5 以上を検知したい場合は 4 を指定する場合がありますのでご注意ください。

max-complexity はコマンドラインオプションの他に .flake8 ファイルでも指定可能です。

[flake8]
max-complexity = 4

以下のようにコメントを書くと特定箇所のみチェックを無視することも可能です。

def fizzbuzz():  # noqa: C901
    for i in range(1, 100+1):
        if i % 3 == 0 and i % 5 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)

まとめ

McCabe を使った Python コードの複雑度のチェック方法についてご紹介しました。複雑度を継続的にチェックしてメンテナンス性を保っていきましょう!

あわせて読みたい

blog.serverworks.co.jp

blog.serverworks.co.jp