使用Python3实现判断函数的圈复杂度

 更新时间:2024年04月25日 11:11:40   作者:rs勿忘初心  
编写函数最重要的原则就是:别写太复杂的函数,那什么样的函数才能算是过于复杂?一般会通过两个标准来判断,长度和圈复杂度,下面我们就来看看如何使用Python判断函数的圈复杂度吧

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

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

长度

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

但即便如此,长度对于判断函数复杂度来说仍然有巨大价值。在著作《代码大全(第 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 命令检查函数圈复杂度是否符合标准,而会将这种检查配置到开发或部署流程中自动执行。

到此这篇关于使用Python3实现判断函数的圈复杂度的文章就介绍到这了,更多相关Python3函数圈复杂度内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用OpenCV circle函数图像上画圆的示例代码

    使用OpenCV circle函数图像上画圆的示例代码

    这篇文章主要介绍了使用OpenCV circle函数图像上画圆的示例代码,本文内容简短,给大家突出重点内容,需要的朋友可以参考下
    2019-12-12
  • python pandas中索引函数loc和iloc的区别分析

    python pandas中索引函数loc和iloc的区别分析

    在数据分析过程中,很多时候我们需要从数据表中提取出我们需要的部分,而这么做的前提是我们需要先索引出这一部分数据,下面这篇文章主要给大家介绍了关于python pandas中索引函数loc和iloc区别的相关资料,需要的朋友可以参考下
    2021-09-09
  • 使用Python从零实现一个最基础的MCP协议

    使用Python从零实现一个最基础的MCP协议

    模型上下文协议是 Anthropic 在2024年提出的一种开放标准协议,用于标准化 AI 模型与外部工具和数据源的集成方式,本示例将展示如何使用 Python 实现一个最基础的 MCP 协议,包括 MCP 服务器和 MCP 客户端两部分,需要的朋友可以参考下
    2026-04-04
  • python如何实现单链表的反转

    python如何实现单链表的反转

    这篇文章主要介绍了python如何实现单链表的反转,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 如何用Python 实现景区安防系统

    如何用Python 实现景区安防系统

    本设计中,利用YOLO目标检测算法、Openpose姿态识别算法、deepsort跟踪算法、MSCNN人群密度估计算法实现了火灾监测、吸烟监测、行为安全监测、人群密度监测、口罩率监测、人员定位监测六大功能,对Python 实现景区安防系统感兴趣的朋友一起看看吧
    2022-07-07
  • python pyhs2 的安装操作

    python pyhs2 的安装操作

    这篇文章主要介绍了python pyhs2 的安装操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Python numpy中np.random.seed()的详细用法实例

    Python numpy中np.random.seed()的详细用法实例

    在学习人工智能时,大量的使用了np.random.seed(),利用随机数种子,使得每次生成的随机数相同,下面这篇文章主要给大家介绍了关于Python numpy中np.random.seed()的详细用法,需要的朋友可以参考下
    2022-08-08
  • 深入浅析Python中的迭代器

    深入浅析Python中的迭代器

    迭代器是实现了迭代器协议的类对象,迭代器协议规定了迭代器类必需定义__next()__方法。这篇文章主要介绍了Python中的迭代器,需要的朋友可以参考下
    2019-06-06
  • 使用Python提取PDF大纲(书签)的完整指南

    使用Python提取PDF大纲(书签)的完整指南

    PDF大纲(Outline)​​ 是PDF文档中的导航结构,通常显示在阅读器的侧边栏中,方便用户快速跳转到文档的不同部分,大纲通常以层级结构组织,包含标题和对应的页面位置,本文给大家介绍了使用Python提取PDF大纲(书签)的完整指南,需要的朋友可以参考下
    2025-08-08
  • python创建和删除目录的方法

    python创建和删除目录的方法

    这篇文章主要介绍了python创建和删除目录的方法,涉及Python操作目录的相关技巧,非常具有实用价值,需要的朋友可以参考下
    2015-04-04

最新评论