使用Python3实现判断函数的圈复杂度
作者:rs勿忘初心
你有没有见过那种长达几百行、逻辑错综复杂的“巨无霸”函数?那样的函数不光难读,改起来同样困难重重,人人唯恐避之不及。
编写函数最重要的原则就是:别写太复杂的函数。那什么样的函数才能算是过于复杂?一般会通过两个标准来判断,长度和圈复杂度。
长度
长度也就是函数有多少行代码。不过不能武断地说,长函数就一定比短函数复杂。因为在不同的编程风格下,相同行数的代码所实现的功能可以有巨大差别,有人甚至能把一个完整的俄罗斯方块游戏塞进一行代码内。
但即便如此,长度对于判断函数复杂度来说仍然有巨大价值。在著作《代码大全(第 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函数圈复杂度内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!