Python3:函数的圈复杂度

你有没有见过那种长达几百行、逻辑错综复杂的“巨无霸”函数?那样的函数不光难读,改起来同样困难重重,人人唯恐避之不及。

编写函数最重要的原则就是:别写太复杂的函数。那什么样的函数才能算是过于复杂?一般会通过两个标准来判断,长度和圈复杂度

长度

长度也就是函数有多少行代码。不过不能武断地说,长函数就一定比短函数复杂。因为在不同的编程风格下,相同行数的代码所实现的功能可以有巨大差别,有人甚至能把一个完整的俄罗斯方块游戏塞进一行代码内。

但即便如此,长度对于判断函数复杂度来说仍然有巨大价值。在著作《代码大全(第 2 版)》中,Steve McConnell 提到函数的理想长度范围是 65 到 200 行,一旦超过 200 行,代码出现 bug 的概率就会显著增加。

对于 Python 这种强表现力的语言来说,65 行已经非常值得警惕了。假如你的函数超过 65 行,很大概率代表函数已经过于复杂,承担了太多职责,请考虑将它拆分为多个小而简单的子函数(类)吧。

圈复杂度

“圈复杂度”是由 Thomas J. McCabe 在 1976 年提出的用于评估函数复杂度的指标。它的值是一个正整数,代表程序内线性独立路径的数量。圈复杂度的值越大,表示程序可能的执行路径就越多,逻辑就越复杂。

如果某个函数的圈复杂度超过10,就代表它已经太复杂了,代码编写者应该想办法简化。优化写法或者拆分成子函数都是不错的选择。接下来,我们通过实际代码来体验一下圈复杂度的计算过程。

在Python中,可以通过radon工具计算函数的圈复杂度。安装命令:

pip3 install radon

假设我们有段代码示例如下,实现的功能是猜数字游戏,里面有1个whilie和2个if-else分支判断逻辑,文件名:complex_func.py。

import random

def guess_number():
    # 生成一个随机数作为答案
    answer = random.randint(1, 100)

    # 初始化猜测次数
    guesses = 0

    print("欢迎来到猜数字游戏!我已经想好了一个1到100之间的数字,你需要猜出这个数字是多少。")

    # 开始循环,直到玩家猜中数字为止
    while True:
        # 获取玩家的猜测
        guess = int(input("请输入你猜测的数字:"))

        # 增加猜测次数
        guesses += 1

        # 检查玩家猜测的数字与答案的关系
        if guess < answer:
            print("你猜的数字太小了,请继续努力!")
        elif guess > answer:
            print("你猜的数字太大了,请再试一次!")
        else:
            print(f"恭喜你,你猜对了!答案是 {answer}。你一共猜了 {guesses} 次。")
            break  # 结束循环


# 调用函数开始游戏
guess_number()

接下来我们使用radon来计算这个文件对应函数的圈复杂度,文件名:calculate_cyclomatic_complexity.py

from radon.complexity import cc_visit

# 定义一个Python文件路径
file_path = 'complex_func.py'

# 使用cc_visit函数计算代码的圈复杂度
with open(file_path, 'r') as file:
    code = file.read()
    results = cc_visit(code)
    print(results)

# 打印结果
for result in results:
    print(result)

执行结果:可以看到函数圈复杂度为 4

$ python3 calculate_cyclomatic_complexity.py 
[Function(name='guess_number', lineno=3, col_offset=0, endline=27, is_method=False, classname=None, closures=[], complexity=4)]
F 3:0->27 guess_number - 4

我们接下来看另外一个完整的代码示例,其中被计算的函数为rank(),功能是按照电影分数计算评级,最后输出了圈复杂度和对应的评分等级,文件名:get_film_score.py

import radon
from radon.complexity import cc_rank, cc_visit


def calculate_complexity(source_code):
    """
    Calculate the cyclomatic complexity of the given source code.

    Parameters:
    source_code (str): The source code to analyze.

    Returns:
    int: The cyclomatic complexity.
    str: The complexity rating.
    """
    try:
        # Visit the AST and calculate the complexity
        results = cc_visit(source_code)
        complexity = results[0].complexity
        # Get the complexity rating
        rating = cc_rank(complexity)
        return complexity, rating
    except Exception as e:
        print("Error:", e)
        return None, None


# Example usage:
if __name__ == "__main__":
    code = """
def rank(self):
    rating_num = float(self.rating)
    if rating_num >= 8.5:
        return 'S'
    elif rating_num >= 8:
        return 'A'
    elif rating_num >= 7:
        return 'B'
    elif rating_num >= 6:
        return 'C'
    else:
        return 'D'
    """
    complexity, rating = calculate_complexity(code)
    if complexity is not None and rating is not None:
        print("Cyclomatic Complexity:", complexity)
        print("Complexity Rating:", rating)

运行结果:可以看到函数圈复杂度为 5,评级为 A

虽然这个值没有达到危险线 10,但考虑到函数只有短短 10 行,5 已经足够引起重视了。

$ python3 get_film_score.py
Cyclomatic Complexity: 5
Complexity Rating: A

作为对比,我们再计算一下案例中使用bisect模块重构后的 rank() 函数

def rank(self):
    breakpoints = (6, 7, 8, 8.5)
    grades = ('D', 'C', 'B', 'A', 'S')
    index = bisect.bisect(breakpoints, float(self.rating))
    return grades[index]

运行结果:可以看到函数圈复杂度为 1,评级为 A。

$ python3 get_film_score.py
Cyclomatic Complexity: 1
Complexity Rating: A

可以看到,新函数的圈复杂度从 5 降至 1。1 是一个非常理想化的值,如果一个函数的圈复杂度为 1,就代表这个函数只有一条主路径,没有任何其他执行路径,这样的函数通常来说都十分简单、容易维护。

当然,在正常的项目开发流程中,我们一般不会在每次写完代码后,都手动执行一次 radon 命令检查函数圈复杂度是否符合标准,而会将这种检查配置到开发或部署流程中自动执行。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rs勿忘初心

您的鼓励将是我的最大创动原动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值