Skip to content

Python 注释与文档工程指南

Python 3.10+Documentation

注释是代码非常重要的组成部分。它们不影响代码的实际行为,但直接影响代码的可读性和可维护性。优秀的注释能帮助读者快速理解代码意图,而糟糕的注释则会造成困惑甚至误导。

Code tells you how, comments tell you why.


一、📝 注释基础知识 (Comment Fundamentals)

1.1 注释类型概览

Python 中有两种主要的注释形式:

类型语法用途运行时可访问
代码注释# comment解释代码逻辑
文档字符串"""docstring"""描述模块/类/函数✅ (__doc__)

1.2 代码内注释 (#)

使用 # 号的注释是最常见的形式:

python
# 单行注释:解释下一行代码
user_input = user_input.strip()

# 多行注释:每行都需要 #
# 使用 strip() 去掉空格的原因:
# 1. 数据库保存时占用空间更小
# 2. 避免因用户误输入空格导致验证失败
username = extract_username(user_input.strip())

行内注释

行内注释放在语句末尾,与代码至少隔 2 个空格:

python
x = x + 1  # 补偿边界条件

PEP 8 建议

行内注释应谨慎使用。如果注释只是在陈述显而易见的事情,它反而会分散注意力。

1.3 文档字符串 (Docstrings)

Docstring 是 Python 特有的文档机制(PEP 257),使用三引号包裹,放在模块、类或函数定义的第一行:

python
def calculate_area(radius: float) -> float:
    """计算圆的面积。
    
    Args:
        radius: 圆的半径,必须为正数。
        
    Returns:
        圆的面积。
    """
    return 3.14159 * radius ** 2

Docstring 在运行时可通过 __doc__ 属性访问:

python
>>> print(calculate_area.__doc__)
计算圆的面积。
...

二、🚫 注释的常见错误 (Common Mistakes)

2.1 用注释屏蔽代码

反模式:把注释当作临时屏蔽代码的工具:

python
# ❌ 源码里有大段大段被注释掉的代码
# order = get_order(request)
# order.validate()
# order.process()
# ... 100 行被注释的代码 ...

问题:这些"僵尸代码"对阅读者毫无意义,只会造成干扰。

解决方案:直接删除不需要的代码。如果未来需要,可以从 Git 历史中找回——这正是版本控制系统存在的意义。

2.2 用注释复述代码

反模式:注释只是重复代码本身做了什么:

python
# ❌ Bad: 调用 strip() 去掉空格
user_input = user_input.strip()

# ❌ Bad: 将 x 加 1
x = x + 1

问题:读者从代码本身就能获取这些信息,注释毫无价值。

好的注释应该解释"为什么":

python
# ✅ Good: 如果带空格的输入直接传到后端,可能导致验证失败
user_input = user_input.strip()

# ✅ Good: 补偿数组从 0 开始索引的偏移
x = x + 1

2.3 指引性注释 vs 提炼函数

指引性注释简明扼要地概括代码功能,起到"代码导读"的作用:

python
# 初始化服务客户端
token = auth_service.get_token()
client = ServiceClient(token=token)
client.connect()

# 获取并过滤数据
data = client.fetch_all()
filtered = [item for item in data if item.is_valid]

但需要判断:何时该写注释,何时该提炼为独立函数?

上面的代码可以重构为:

python
client = create_service_client()
filtered = fetch_and_filter_data(client)

有意义的函数名已经达到了概括和指引的作用。

权衡原则

  • 如果逻辑复用性高 → 提炼函数
  • 如果只是帮助理解上下文 → 指引性注释也可接受
  • 无论代码写得多好,读注释通常比读代码更轻松

2.4 弄错 Docstring 的受众

反模式:在 Docstring 中过多阐述实现细节:

python
# ❌ 实现细节不应放在 Docstring
def resize_image(image, size):
    """将图片缩放到指定尺寸。
    
    该函数使用 Pillow 模块读取文件对象,然后调用 resize() 方法。
    由于 Pillow 内存分配问题,当文件超过 5MB 时性能会急剧下降,
    这是因为 malloc 在处理大块内存时...(省略 200 字实现细节)
    
    Args:
        image: 图片文件对象
        size: (width, height) 元组
    """

问题:Docstring 是给使用者看的,不是给维护者看的。

正确写法

python
# ✅ 简明扼要,面向使用者
def resize_image(image, size):
    """将图片缩放到指定尺寸并返回新图片。
    
    Note:
        当文件超过 5MB 时,建议使用 resize_large_image() 以获得更好性能。
    
    Args:
        image: 图片文件对象
        size: (width, height) 元组
        
    Returns:
        缩放后的新图片对象
    """

实现细节可以放在函数内部的代码注释中。


三、📋 Docstring 规范 (Docstring Standards)

PEP 257 定义了 Docstring 的基本规范。在此基础上,社区发展出了多种流行风格。

3.1 三种主流风格对比

Google Style(推荐)

简洁易读,适合大多数项目:

python
def fetch_user(user_id: int, include_orders: bool = False) -> dict:
    """从数据库获取用户信息。

    如果缓存命中,优先返回缓存数据。

    Args:
        user_id: 用户唯一标识符。
        include_orders: 是否包含历史订单。默认 False,
            开启后可能增加 200ms 延迟。

    Returns:
        包含用户数据的字典,例如 {'id': 1, 'name': 'Alice'}。

    Raises:
        ValueError: 当 user_id 无效时。
        ConnectionError: 当数据库连接失败时。
    """

Sphinx/reST Style

与 Sphinx 文档生成器原生兼容:

python
def fetch_user(user_id: int, include_orders: bool = False) -> dict:
    """从数据库获取用户信息。

    :param user_id: 用户唯一标识符
    :type user_id: int
    :param include_orders: 是否包含历史订单
    :type include_orders: bool
    :returns: 包含用户数据的字典
    :rtype: dict
    :raises ValueError: 当 user_id 无效时
    """

NumPy Style

常用于科学计算库:

python
def fetch_user(user_id, include_orders=False):
    """从数据库获取用户信息。

    Parameters
    ----------
    user_id : int
        用户唯一标识符
    include_orders : bool, optional
        是否包含历史订单 (default is False)

    Returns
    -------
    dict
        包含用户数据的字典
    """

3.2 风格选择建议

场景推荐风格
通用项目Google Style
使用 Sphinx 生成文档Sphinx Style 或 Google + Napoleon
科学计算 / NumPy 生态NumPy Style

Sphinx + Napoleon

如果你使用 Google Style 但需要 Sphinx 生成文档,可以启用 sphinx.ext.napoleon 扩展,它会自动将 Google Style 转换为 reST 格式。

3.3 Docstring 内容要点

一份完整的 Docstring 应包含:

  1. 功能概述:一句话描述对象做什么
  2. 详细说明(可选):补充背景、使用场景
  3. Args/Parameters:参数说明
  4. Returns/Yields:返回值说明
  5. Raises:可能抛出的异常
  6. Examples(可选):使用示例
  7. Notes/Warnings(可选):注意事项

四、🎯 文档驱动设计 (Documentation-Driven Design)

4.1 先写注释,后写代码

这是一个值得推广的工程习惯:将 Docstring 作为设计工具

  1. 在写代码前,先尝试写出函数的 Docstring
  2. 如果你发现无法用几句话清晰描述函数的职责,这通常意味着函数设计不合理
  3. 通过这种"文档驱动设计",可以在写代码之前就发现架构问题

4.2 示例

假如你准备实现一个 process_order() 函数。在写 Docstring 时,你发现需要描述:

  • 验证订单格式
  • 计算价格
  • 检查库存
  • 发送通知
  • 记录日志

这说明 process_order() 承担了太多职责。解决办法是拆分为多个单一职责的函数:

python
def validate_order(order: Order) -> bool:
    """验证订单格式是否有效。"""

def calculate_total(order: Order) -> Decimal:
    """计算订单总价。"""

def check_inventory(order: Order) -> bool:
    """检查订单商品库存是否充足。"""

4.3 避免遗漏 Docstring

程序员常常跳过 Docstring 直接写代码。写完后对函数失去兴趣,不愿回头补文档。

解决方案:遵守"先写注释,后写代码"的习惯——在写出有说服力的 Docstring 前,不写任何函数代码。


五、📐 空行也是"注释" (Whitespace as Documentation)

代码里的"注释"不只是描述性文字。空行也是一种特殊的视觉组织工具。

5.1 无空行 vs 有空行

python
# ❌ 没有空行,代码像砖墙一样密集
def process(numbers: list[int]) -> list[int]:
    result = []
    for n in numbers:
        if n % 2 == 0:
            result.append(n * 2)
        else:
            result.append(n)
    total = sum(result)
    average = total / len(result)
    return result
python
# ✅ 适当的空行增加"呼吸感"
def process(numbers: list[int]) -> list[int]:
    result = []
    
    for n in numbers:
        if n % 2 == 0:
            result.append(n * 2)
        else:
            result.append(n)
    
    total = sum(result)
    average = total / len(result)
    
    return result

5.2 PEP 8 空行规范

场景空行数
顶层函数/类定义之间2 行
类内方法之间1 行
函数内逻辑块之间1 行(适度)

六、🔧 Docstring 与类型注解的关系

6.1 互补而非替代

类型注解提供机器可读的类型信息:

python
def greet(name: str, age: int) -> str:
    ...

Docstring提供人类可读的语义信息:

python
def greet(name: str, age: int) -> str:
    """生成个性化问候语。
    
    Args:
        name: 用户姓名,用于称呼
        age: 用户年龄,用于选择敬语
    """

6.2 最佳实践

信息类型放置位置
参数/返回值类型类型注解(函数签名)
参数/返回值含义Docstring
参数约束(如范围、格式)Docstring
异常说明Docstring
python
def calculate_discount(price: float, rate: float) -> float:
    """计算折扣后价格。
    
    Args:
        price: 原价,必须 > 0
        rate: 折扣率,范围 0.0 ~ 1.0
        
    Returns:
        折扣后的价格
        
    Raises:
        ValueError: 当 price <= 0 或 rate 不在有效范围时
    """

七、✅ 最佳实践清单 (Checklist)

优先级检查项描述
P0无僵尸代码是否删除了所有被注释掉的无用代码?
P0解释"为什么"注释是否解释了代码背后的原因,而非复述代码?
P1Docstring 覆盖公共函数和类是否都有 Docstring?
P1受众明确Docstring 是否面向使用者,而非实现细节?
P2风格统一项目内 Docstring 风格是否统一?
P2空行分块是否使用空行将代码按逻辑分块?
P3文档先行是否在写代码前先写 Docstring?

八、📚 延伸阅读


← 返回 Python 深度研究