McCabe とは
Python コードの複雑さをチェックしてくれるモジュールです。複雑さをチェックすることで、保守性の悪いコードをリリース前に検知することが可能です。もちろん、コードレビューで検知することも出来ますが、複雑と感じるかは個人差がありますので、予めチーム内で指標を決めて機械的にチェックする方が生産性が高そうです。
尚、複雑さ
は具体的には 循環的複雑度
と呼ばれる指標を用います。
循環的複雑度とは
循環的複雑度(英: Cyclomatic complexity)とは、ソフトウェア測定法の一種である。Thomas McCabe が開発したもので、プログラムの複雑度を測るのに使われる。プログラムのソースコードから、線形的に独立した経路の数を直接数える。
具体的な定義としては以下の通りです。
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; } }
可視化してみると以下の結果が得られました。丸の中の数字はファイル内の行数です。初見の複雑なコードリーディングにも役立つかもしれないですね。
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 コードの複雑度のチェック方法についてご紹介しました。複雑度を継続的にチェックしてメンテナンス性を保っていきましょう!