サイクロマチック数を活用してPythonコードを評価する

はじめに

自分でかいたコードや、他の人のコードレビューをするとき、「なんとなくみにくいコードだけど、どこまでリファクタリングすべき?」という疑問がよくでてきます。

仮に、コードのみにくさ、つまり複雑性を定量化できれば、「このコードの複雑性がxxになるまではリファクタリングしよう」という判断ができますよね。

そんな願いを叶えてくれる指標がMcCabeのサイクロマチック数という指標です。今回はPythonのコードを例にサイクロマチック数をつかって、コードの複雑性を評価したいと思います。

サイクロマチック数とは?

そもそも、サイクロマチック数とは、コードの複雑性を定量的に測定することのできる指標で、この指標の増加はテストすべき処理の増加を意味します。つまり、数値が大きいほど、コードの可読性、保守性が悪いということです。

目安としては、以下がよく使われるようです。

サイクロマチック数 評価
1 - 10 シンプルなプログラム、リスクが低い
11 - 20 より複雑、中程度のリスク
21 - 50 複雑、高リスク
> 50 テスト不能なコード、非常に高リスク

より具体的には、for, if, whileが多いほど数が増えていきます。条件式やループが多いコードにバグが多いのは、明白ですよね。

実際に使ってみる。

サイクロマチック数を測定するパッケージはいくつかありますが、今回はmccabパッケージを使っていきます。 サンプルコードとして、test1~7まで順に複雑にした関数と一つのクラスを準備します。

def test1(x):  # シンプルプログラム
    print(x)

def test2(x):  # if分を追加
    if x == 1:
        print("one")
    else:
        print("other")

def test3(x):  # 入れ子の関数を追加
    def tmp():  ##
        print(x)
    tmp()  ##
    if x == 1:
        print("one")
    else:
        print("other")

def test4(x):  # 条件分岐を追加
    def tmp():
        print(x)
    tmp()
    if x == 1:
        print("one")
    elif x == 2:  ##
        print("two")
    else:
        print("other")

def test5(x):  # for文を追加
    def tmp():
        print(x)
    tmp()
    if x == 1:
        print("one")
    elif x == 2:
        print("two")
    else:
        print("other")

    for i in range(x):  ##
        print(i)

def test6(x):  # 条件式を追加
    def tmp():
        print(x)
    tmp()
    if x == 1:
        print("one")
    elif x == 2 or x == 3:  ##
        print("two, three")
    else:
        print("other")

    for i in range(x):
        print(i)

def test7(x):  ## for文を2重
    def tmp():
        print(x)
    tmp()
    if x == 1:
        print("one")
    elif x == 2 or x == 3:
        print("two, three")
    else:
        print("other")

    for i in range(x):
        for j in range(x):  ##
            print(j, i)

class Test:  ## クラスを追加
    def __init__(self, x):
        self.x = x

def test(self):
        print(self.x)

このコードを例にサイクロマチック数を測定した結果が以下です。test5~test6では、if分の条件式をいじっただけですので、サイクロマチック数は増えていないようです。また、classの場合は、各メソッドごとに測定されていますね。

python -m mccabe sample_functions.py
1:0: 'test1' 1
5:0: 'test2' 2
12:0: 'test3' 3
24:0: 'test4' 4
38:0: 'test5' 5
55:0: 'test6' 5
72:0: 'test7' 6
91:4: 'Test.__init__' 1
94:4: 'Test.test' 1
# 表示する最小のサイクロマチック数を指定することも
python -m mccabe --min 5 sample_functions.py
38:0: 'test5' 5
55:0: 'test6' 5
72:0: 'test7' 6

このように簡単にコードの複雑性を定量化することができます。

まとめ

コードのリファクタリングって、職人技で沼にハマりやすく無限に時間が使えるなと思っています。この指標をつかえば、各処理やスクリプトでどれぐらいのテストケースが必要なのか、どこまで処理をシンプルにすればよいかが定量化できるので、チーム内でのコミュニケーションにも使えそうですね。