你有没有见过那种长达几百行、逻辑错综复杂的“巨无霸”函数?那样的函数不光难读,改起来同样困难重重,人人唯恐避之不及。
编写函数最重要的原则就是:别写太复杂的函数。那什么样的函数才能算是过于复杂?一般会通过两个标准来判断,长度和圈复杂度。
长度
长度也就是函数有多少行代码。不过不能武断地说,长函数就一定比短函数复杂。因为在不同的编程风格下,相同行数的代码所实现的功能可以有巨大差别,有人甚至能把一个完整的俄罗斯方块游戏塞进一行代码内。
但即便如此,长度对于判断函数复杂度来说仍然有巨大价值。在著作《代码大全(第 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 命令检查函数圈复杂度是否符合标准,而会将这种检查配置到开发或部署流程中自动执行。