Python 代码质量:最佳实践与工具(1)

定义代码质量

谁都希望能写出高质量的代码,但什么是代码质量?事实证明,对这个术语可谓是仁者见仁、智者见智。

一种理解方式是从质量的两个极端来分析:

  • 低质量代码:它具有最低限度要求的特性以实现功能性
  • 高质量代码:它具备所有必要的特性,能够可靠、高效且有效地运行,同时还易于维护。

在接下来的内容中,将就这两种质量分类及其定义特征给予详细讨论。

低质量代码

低质量代码通常仅具备最低限度的功能性特征。它可能不够优雅、不够高效,也不易于维护,但至少满足以下基本标准:

  • 能够完成预期的任务。如果代码无法满足需求,那么它就不能被称为有一定质量代码。开发软件是为了完成某项任务,如果它无法做到这一点,就不能被视为合格的代码。
  • 不包含严重错误。如果代码存在缺陷、错误或导致问题,很可能不会认为它是满足某种质量的代码。如果质量太低以至于无法使用,那么它甚至达不到基本的质量标准,可能不会使用它。

尽管简单,这两个特征通常被认为是功能性低质量代码的底线。低质量代码或许可以运行,但它往往缺乏可读性、可维护性和效率,因此难以扩展或改进。

高质量代码

以下是一份定义高质量代码的关键特征的列表:

  • 功能性:按预期工作并实现其预期目标。
  • 可读性:易于人类理解。
  • 文档化:清晰地解释其用途和用法。
  • 遵循标准:遵守约定和规范,例如 PEP 8。
  • 可重用性:可以在不同场景中使用而无需修改。
  • 可维护性:允许进行修改和扩展而不引入错误。
  • 鲁棒性:能够有效处理错误和意外输入。
  • 可测试性:可以轻松验证其正确性。
  • 高效性:优化时间和资源的使用。
  • 可扩展性:在数据量增加或复杂性提升时不会出现性能下降。
  • 安全性:防止漏洞和恶意输入。

简而言之,高质量代码是功能完善、易读、易维护且健壮的。它遵循最佳实践,包括清晰的命名、一致的编码风格、模块化设计、适当的错误处理以及对编码标准的遵守。此外,高质量代码文档齐全,易于测试和扩展。最后,高质量代码具备高效性和安全性,确保可靠性和安全使用。

上述所有特征使开发人员能够以最小的努力理解、修改和扩展 Python 代码库。

代码质量的重要性

要理解为什么代码质量很重要,结合前面所列举的高质量代码的特征,对代码可以有如下类型解读:

  • 功能性代码:确保正确的行为和预期的结果。
  • 可读性代码:使代码更易于理解和维护。
  • 文档化代码:阐明他人使用代码的正确和推荐方式。
  • 合规性代码:促进一致性并支持协作。
  • 可重用性代码:通过代码复用节省时间。
  • 可维护性代码:轻松支持更新、改进和扩展。
  • 健壮性代码:减少崩溃并产生更少的边界问题。
  • 可测试性代码:通过代码测试简化正确性的验证。
  • 高效性代码:运行更快并节约系统资源。
  • 可扩展性代码:支持项目增长和数据量增加。
  • 安全性代码:提供针对系统漏洞和恶意输入的保护。

代码质量之所以重要,是因为它能够生成更容易理解、修改和扩展的代码。随着时间的推移,高质量代码可以加快调试速度、推动功能开发更加顺畅、降低成本,并提高用户满意度,同时确保安全性和可扩展性。

通过示例演示 Python 中的代码质量

在接下来的部分中,你将看到一些简短的代码示例,这些示例清楚地展示高质量 Python 代码各项特征的重要性。

功能性

评估一段代码质量时,最重要的因素是它是否能够完成其预期的任务。如果这一点未能实现,那么就没有讨论代码质量的余地。

以下是一个快速示例,展示了一个用于加两个数字的函数。我们将从该函数的一个低质量实现开始。

🔴 低质量带代码:

>>> def add_numbers(a, b):
...     return a + b
...

>>> add_numbers(2, 3)
5

测试 add_numbers() 函数,貌似不错。然而,如果输入的参数类型不是整数或浮点数,如下所示,包括一个“字符数字”,此函数就会报错了。

>>> add_numbers(2, "3")
Traceback (most recent call last):
    ...
TypeError: unsupported operand type(s) for +: 'int' and 'str'

那么,高质量代码应怎么写呢?

高质量代码:

>>> def add_numbers(a: int | float, b: int | float) -> float:
...     a, b = float(a), float(b)
...     return a + b
...

>>> add_numbers(2, 3)
5.0

>>> add_numbers(2, "3")
5.0

在新的函数中,通过参数中的类型注释就知道,该函数现在应该使用 intfloat 类型的数值调用。当用数字调用它时,它会按预期工作。

那么,如果违反了参数类型会怎样?函数中将输入参数转换为 float 类型的数字。通过这种方式,即使输入类型不是预期的类型(例如字符串形式的数字),函数也会更具弹性并能够接受这些值。

当然,这种实现并不完美,但从功能角度来看,它比第一个实现更好。

可读性

代码可读性是 Python 的核心原则之一。从一开始,Python 的创造者 Guido van Rossum 就强调了它的重要性,而如今它仍然是核心开发者和社区的优先事项。这一点甚至体现在 《Python 之禅》 中:

Readability counts. 可读性很重要。(来自《Python之禅》)

以下示例展示了为什么可读性很重要。

🔴 低质量代码:

>>> def ca(w, h):
...     return w * h
...

>>> ca(12, 20)
240

此函数能够正常执行,它接受两个数字,并返回它们相乘的结果。但是,从上面的代码中,你能知道这个函数是出于什么目的而编写的吗?这就是上述代码需要优化的地方。

高质量代码:

>>> def calculate_rectangle_area(width: float, height: float) -> float:
...     return width * height
...

>>> calculate_rectangle_area(12, 20)
240

现在看到的是同样的函数,但是你立刻就知道这个函数的编写目的和功能,函数名已经非常明白地显示出来了。

文档化

在软件开发者中,编写代码文档是一项不太受重视的任务。然而,清晰且结构良好的文档对于评估任何软件项目的质量至关重要。下面是一个展示文档如何提升代码质量的示例。

🔴 低质量代码:

>>> def multiply(a, b):
...     return a * b
...

>>> multiply(2, 3)
6

这个函数没有对参数或返回值提供任何解释。如果深入研究代码,可以弄清楚函数的作用,但如果有更多的上下文信息会更好。而这正是文档的作用所在。下面的改进版本使用了文档字符串(docstrings)和类型提示(type hints)来为代码提供文档支持。

高质量代码

>>> def multiply(a: float, b: float) -> float:
...     """Multiply two numbers.
...     Args:
...         a (float): First number.
...         b (float): Second number.
...     Returns:
...         float: Product of a and b.
...     """
...     return a * b
...

>>> multiply(2, 3)
6

在函数的文档中,提供了上下文信息,让其他人了解该函数的作用、它应该接收的输入类型,还明确了返回值及其对应的数据类型。

符合标准

符合知名且广泛接受的代码标准是影响代码质量的另一个关键因素。相关的标准会因具体项目的不同而有所变化。一个良好的通用示例是编写遵循 PEP 8 中确立的标准和规范的 Python 代码,这是 Python 代码的官方风格指南。以下是一个未遵循 PEP 8 指南的低质量代码示例。

🔴 低质量代码:

>>> def calcTotal(price,taxRate=0.05): return price*(1+taxRate)
...

>>> calcTotal(1.99)
2.0895

这个函数没有遵循 PEP 8 中规定的命名约定和空格规范。代码可能可以运行,但它看起来并不像高质量的 Python 代码。它不够“Pythonic”(符合 Python 风格)。下面是改进后的版本。

高质量代码:

>>> def calculate_price_with_taxes(
...     base_price: float, tax_rate: float = 0.05
... ) -> float:
...     return base_price * (1 + tax_rate)
...

>>> calculate_total_price(1.99)
2.0895

在这里,函数遵循了推荐的命名约定,使用“蛇形命名法”来命名函数和变量。同时,它还在符号之间使用了适当的空格,并保持了一致的行长度规范。

可重用性

可重用性也是高质量代码的一项基本特征。可重用的代码减少了重复,从而提高了可维护性,并对生产力产生重要影响。

🔴 低质量代码:

>>> def greet_alice():
...     return "Hello, ZhangSan!"
...

>>> greet_alice()
'Hello, ZhangSan!'

这个函数对其用例进行了硬编码,它只能用于对 ZhangSan 打招呼,这就非常局限。请看下面的改进版本。

高质量代码

>>> def greet(name: str) -> str:
...     return f"Hello, {name}!"
...

>>> greet("ZhangSan")
'Hello, ZhangSan!'
>>> greet("LiSi")
'Hello, LiSi!'
>>> greet("WangWu")
'Hello, WangWu!'

虽然这个函数非常基础,但它比之前的版本更通用、更有用。以人的名字作为参数,并使用 f-string 构建问候消息。

可维护性

可维护性是指编写出你或其他人能够快速理解、更新、扩展和修复的代码。避免重复代码以及避免让代码承担多重职责,是实现这一质量特性的关键原则。请看下面的示例。

🔴 低质量代码:

>>> def process(numbers):
...     cleaned = [number for number in numbers if number >= 0]
...     return sum(cleaned)
...

>>> print(process([1, 2, 3, -1, -2, -3]))
6

尽管这个函数非常简短,但它承担了多项职责。首先,它通过过滤掉负数来清理输入数据;然后,它计算总和并将其返回给调用者。再来看下面的改进版本。

高质量代码:

>>> def clean_data(numbers: list[int]) -> list[int]:
...     return [number for number in numbers if number >= 0]
...

>>> def calculate_total(numbers: list[int]) -> int:
...     return sum(numbers)
...

>>> cleaned = clean_data([1, 2, 3, -1, -2, -3])
>>> print(calculate_total(cleaned))
6

这次,用一个函数专门清理数据,用另一个函数计算总和。每个函数都具有单一职责,因此它们更易于维护,也更容易理解。

健壮性

编写健壮的代码在 Python 或其他任何语言中都是至关重要的。健壮的代码能够优雅地处理错误,防止程序崩溃以及出现意外行为和结果。请看下面的示例,编写一个用于两个数字相除的函数。

🔴 低质量代码:

>>> def divide_numbers(a, b):
...     return a / b
...

>>> divide_numbers(4, 2)
2.0
>>> divide_numbers(4, 0)
Traceback (most recent call last):
    ...
ZeroDivisionError: division by zero

这个函数按预期实现了两个数字的除法。然而,当除数为 0 时,代码会因抛出 ZeroDivisionError 异常而中断。为了解决这个问题,需要处理此异常。

高质量代码:

>>> def divide_numbers(a: float, b: float) -> float | None:
...     try:
...         return a / b
...     except ZeroDivisionError:
...         print("Error: can't divide by zero")
...

>>> divide_numbers(4, 2)
2.0
>>> divide_numbers(4, 0)
Error: can't divide by zero

现在的函数能够处理异常,避免了代码崩溃。还会向用户打印一条错误的提示信息。

可测试性

当一段代码能够让开发者快速编写和运行自动化测试,从而检查代码的正确性时,就可以说它是可测试的。请看下面的简单示例。

🔴 低质量代码:

def greet(name):
    print(f"Hello, {name}!")

这个函数难以测试,因为它使用了内置的 print() 函数,而不是返回一个具体的结果。代码通过“副作用”(side effect)完成操作,这使得测试更加困难。例如,以下是一个利用 pytest 编写的测试:

import pytest

def test_greet(capsys):
    greet("Alice")
    captured = capsys.readouterr()
    assert captured.out.strip() == "Hello, Alice!"

这个测试用例可以工作。然而,由于它需要相对高级的 pytest 库知识,因此编写起来比较困难。

应该将 print() 替换为 return 语句,以提高 greet() 函数的可测试性,并简化测试。

高质量代码:

def greet(name: str) -> str:
    return f"Hello, {name}!"

def test_greet():
    assert greet("Alice") == "Hello, Alice!"

现在,函数返回问候消息。这使得测试用例编写起来更快,并且对 pytest 的知识要求更低。它的运行效率也更高,速度更快,因此这个版本的 greet() 函数更具可测试性。

高效性

在评估一段代码的质量时,高效性是另一个需要考虑的关键因素。通常,一般从时间和空间两个方面来思考高效性。

根据项目需求,还可能有从磁盘使用率、网络延迟、能耗等其他许多方面来评估一段代码的效率。

以下代码用递归算法计算一系列数字的斐波那契数列。

关于斐波那契数列,曾有两个专题文章,供参考:

🔴 低质量代码:

Python 文件 efficiency_v1.py

from time import perf_counter

def fibonacci_of(n):
    if n in {0, 1}:
        return n
    return fibonacci_of(n - 1) + fibonacci_of(n - 2)

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

在命令行中执行上述文件:

$ python efficiency_v1.py
Execution time: 1.83 seconds

大约执行了 2s。下面是优化后的版本:

高质量代码:

Python 文件 efficiency_v2.py

from time import perf_counter

cache = {0: 0, 1: 1}

def fibonacci_of(n):
    if n in cache:
        return cache[n]
    cache[n] = fibonacci_of(n - 1) + fibonacci_of(n - 2)
    return cache[n]

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

这个实现通过缓存优化了斐波那契数的计算。现在,再次从命令行运行这段改进后的代码:

$ python efficiency_v1.py
Execution time: 0.01 seconds

这段代码比之前的版本快了很多。通过提升性能,已经提高了代码的效率。

可扩展性

一段代码的可扩展性指的是它在处理不断增加的工作负载、数据规模或用户需求时,不会影响代码性能、稳定性或可维护性的能力。这是一个相对复杂的概念,为了说明这一点,以下列举一个简单的示例,从中可以观察数据规模增长对代码的影响。

🔴 低质量代码:

>>> def sum_even_numbers(numbers):
...     even_numbers = [number for number in numbers if number % 2 == 0]
...     return sum(even_numbers)
...

>>> sum_even_numbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
30

这个函数的作用是过滤掉列表中的奇数,并创建一个包含所有非奇数的新列表,这些值会全部存储在内存中。当输入列表的规模显著增长时,这可能会成为一个问题。为了使代码在输入数据增长时能够更好地扩展,可以将列表解析式替换为生成器表达式。

高质量代码:

>>> def sum_even_numbers(numbers):
...     return sum(number for number in numbers if number % 2 == 0)
...

>>> sum_even_numbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
30

用生成器表达式实现了过滤奇数的功能,并且将其作为 sum() 的参数,在这个过程中,内存中只有一个值。

安全性

安全的代码能够防止安全漏洞,保护敏感数据,并抵御潜在攻击或恶意输入。遵循最佳实践可以确保系统免受常见安全威胁的影响,保持安全、可靠和弹性。

一个风险代码的典型例子是:在接受用户输入时未对其进行验证,却将该输入用于进一步处理。

🔴 低质量代码:

Python 文件 input_v1.py

user_input = "Amount to withdraw? "
amount = int(input(user_input))
available_balance = 1000
print(f"Here are your {amount:.2f}RMB")
print(f"Your available balance is {available_balance - amount:.2f}RMB")

这段代码使用内置的 input() 函数来获取用户输入。用户应提供要提取的金额:

$ python input_v1.py
Amount to withdrawn? 300
Here are your 300.00RMB
Your available balance is 700.00RMB

该程序接收输入数据,模拟现金提款,并计算最终余额。现在,假设输入了以下内容:

$ python input_v1.py
Amount to withdrawn? 2000
Here are your 2000.00RMB
Your available balance is -1000.00RMB

在这种情况下,代码存在错误,因为输入的值大于可用金额,并且没有进行验证。结果,代码发放了比应有金额更多的钱。虽然这在现实世界中似乎不太可能发生,但它是一个简单而典型的安性漏洞示例。

高质量代码:

user_input = "Amount to withdraw? "
amount = int(input(user_input))
available_balance = 1000
if amount > available_balance:
    print("Insufficient funds")
    amount = 0
else:
    print(f"Here are your {amount:.2f}USD")

print(f"Your available balance is {available_balance - amount:.2f}USD")

在这个更新后的代码中,通过使用条件语句来检查可用余额,从而确保用户提供的金额是有效的。

下一篇:https://cslab.blog.csdn.net/article/details/147067653

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CS创新实验室

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值