目录

Python 文档编写与注释规范

学习目标:掌握 Python 文档字符串(docstring)规范,学会编写清晰的注释,理解项目文档的组成和工具链。


1. 注释 vs 文档字符串

# 注释(Comment):解释代码"怎么做"
# 使用 # 号,面向开发者
total = sum(items)  # 计算总价

# 文档字符串(Docstring):解释"是什么"和"怎么用"
# 使用三引号,面向使用者
def calculate_total(items):
    """计算商品列表的总价。
    
    Args:
        items: 商品列表,每个商品包含 price 字段
    
    Returns:
        所有商品价格之和
    """
    return sum(item['price'] for item in items)

2. 文档字符串规范

2.1 Google 风格(推荐)

def fetch_user(user_id: int, include_profile: bool = False) -> dict:
    """获取用户信息。
    
    根据用户 ID 从数据库获取用户信息,可选择是否包含用户档案。
    
    Args:
        user_id: 用户唯一标识符,必须为正整数。
        include_profile: 是否包含用户档案信息,默认为 False。
    
    Returns:
        包含用户信息的字典,格式如下:
        {
            "id": int,
            "name": str,
            "email": str,
            "profile": dict  # 仅当 include_profile=True
        }
    
    Raises:
        ValueError: 当 user_id 不为正整数时。
        UserNotFoundError: 当用户不存在时。
    
    Examples:
        >>> fetch_user(1)
        {"id": 1, "name": "张三", "email": "zhangsan@example.com"}
        
        >>> fetch_user(1, include_profile=True)
        {"id": 1, "name": "张三", "profile": {"age": 25, "city": "北京"}}
    """
    if user_id <= 0:
        raise ValueError("user_id 必须为正整数")
    
    user = db.get_user(user_id)
    if user is None:
        raise UserNotFoundError(f"用户 {user_id} 不存在")
    
    if include_profile:
        user['profile'] = db.get_profile(user_id)
    
    return user

2.2 类和模块文档

"""用户管理模块。

提供用户注册、登录、信息管理等功能。

示例:
    from user_manager import UserManager
    
    manager = UserManager()
    manager.register("zhangsan", "password123")
"""

class UserManager:
    """用户管理器。
    
    管理用户的注册、认证和信息操作。
    
    Attributes:
        db: 数据库连接实例。
        cache: 缓存实例。
    
    Example:
        >>> manager = UserManager()
        >>> user = manager.login("admin", "password")
        >>> print(user.name)
        管理员
    """
    
    def __init__(self, db_url: str = None):
        """初始化用户管理器。
        
        Args:
            db_url: 数据库连接 URL,默认使用配置文件中的地址。
        """
        self.db = Database(db_url)
        self.cache = {}
    
    def register(self, username: str, password: str) -> dict:
        """注册新用户。
        
        Args:
            username: 用户名,3-20 个字符。
            password: 密码,至少 8 个字符。
        
        Returns:
            新创建的用户信息。
        
        Raises:
            ValueError: 用户名或密码不符合要求。
            UsernameExistsError: 用户名已被注册。
        """
        pass

2.3 NumPy 风格

def linear_regression(X, y, alpha=0.01, iterations=1000):
    """线性回归梯度下降。
    
    使用梯度下降算法拟合线性回归模型。
    
    Parameters
    ----------
    X : array_like
        特征矩阵,形状为 (n_samples, n_features)。
    y : array_like
        目标值,形状为 (n_samples,)。
    alpha : float, optional
        学习率,默认为 0.01。
    iterations : int, optional
        迭代次数,默认为 1000。
    
    Returns
    -------
    theta : ndarray
        拟合后的参数向量,形状为 (n_features,)。
    cost_history : list
        每次迭代的损失值。
    
    Notes
    -----
    学习率过大可能导致不收敛,建议从 0.001 开始调试。
    
    Examples
    --------
    >>> import numpy as np
    >>> X = np.array([[1, 1], [1, 2], [1, 3]])
    >>> y = np.array([2, 4, 6])
    >>> theta, _ = linear_regression(X, y, alpha=0.1)
    """
    pass

3. 注释最佳实践

3.1 好的注释

# 解释"为什么"而不是"做什么"
# 使用二分查找因为数据已排序,O(log n) 比 O(n) 快
def find_item(sorted_list, target):
    left, right = 0, len(sorted_list) - 1
    while left <= right:
        mid = (left + right) // 2
        if sorted_list[mid] == target:
            return mid
        elif sorted_list[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# 标注 workaround 和 TODO
def get_user_data(user_id):
    # TODO: 迁移到新的用户服务 API(预计 v2.0)
    # FIXME: 临时方案,需要处理超时情况
    response = requests.get(f"{OLD_API_URL}/users/{user_id}")
    return response.json()

# 解释复杂的正则表达式
pattern = r'^(\d{4})-(\d{2})-(\d{2})$'  # YYYY-MM-DD 格式日期

# 标注魔法数字的来源
RETRY_INTERVAL = 5  # 根据运维建议,5 秒重试间隔
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB,上传限制

3.2 坏的注释(应避免)

# ✗ 注释只是在重复代码
i += 1  # i 加 1

# ✗ 过时的注释(代码已改但注释没更新)
# 返回用户列表
def get_user_count():  # 实际返回的是数量
    return len(users)

# ✗ 用注释代替函数名
def do_stuff(x, y):
    # 这个函数计算两个数的平均值然后乘以 100
    return (x + y) / 2 * 100

# ✓ 应该改为
def calculate_percentage_average(x, y):
    return (x + y) / 2 * 100

# ✗ 注释掉的代码(用 Git 管理历史)
# old_result = process_old_way(data)
# if old_result > threshold:
#     return old_result
result = process_new_way(data)

4. 类型提示即文档

# 类型提示本身就是最好的文档
# 无需额外注释说明参数类型

def process_order(
    order_id: int,
    items: list[dict[str, int | str]],
    discount: float = 0.0,
    callback: Callable[[int], None] | None = None
) -> dict[str, int | str]:
    """处理订单。
    
    Args:
        order_id: 订单 ID。
        items: 商品列表,每项包含 name(str) 和 quantity(int)。
        discount: 折扣率,0.0-1.0 之间。
        callback: 处理完成后的回调函数。
    
    Returns:
        处理结果,包含 total 和 status。
    """
    pass

5. 项目文档组成

project/
├── README.md           ← 项目入口文档
├── CHANGELOG.md        ← 变更日志
├── CONTRIBUTING.md     ← 贡献指南
├── LICENSE             ← 开源协议
├── docs/               ← 文档目录
│   ├── installation.md ← 安装指南
│   ├── quickstart.md   ← 快速开始
│   ├── api/            ← API 文档
│   └── examples/       ← 示例代码
├── pyproject.toml      ← 项目配置
└── src/
    └── mypackage/
        ├── __init__.py ← 包文档
        └── ...

5.1 README 模板

# 项目名称

一句话描述项目。

## 功能特性

- 特性 1
- 特性 2
- 特性 3

## 安装

pip install package-name

## 快速开始

```python
from package import Module

m = Module()
result = m.run()

文档

完整文档请访问:https://package.readthedocs.io

开发

环境准备

pip install -e “.[dev]”

运行测试

pytest

许可证

MIT License


### 5.2 CHANGELOG 模板

```markdown
# 变更日志

## [1.2.0] - 2024-01-15

### Added
- 新增用户头像上传功能
- 添加批量删除 API

### Changed
- 优化数据库查询性能
- 更新依赖版本

### Fixed
- 修复登录页面在 Safari 上的显示问题
- 修复并发情况下的数据竞争

### Deprecated
- `old_api()` 将在 2.0 版本移除,请使用 `new_api()`

## [1.1.0] - 2023-12-01
...

6. 自动文档生成

6.1 pdoc

# 安装
pip install pdoc

# 生成 HTML 文档
pdoc ./src/mypackage

# 指定输出目录
pdoc ./src/mypackage -o ./docs

# 启动本地服务器预览
pdoc ./src/mypackage --http localhost:8080

6.2 Sphinx

# 安装
pip install sphinx sphinx-autodoc-typehints

# 初始化文档
sphinx-quickstart docs

# 生成 API 文档
sphinx-apidoc -o docs/source ./src/mypackage

# 构建 HTML
cd docs
make html
# docs/source/conf.py
import os
import sys

sys.path.insert(0, os.path.abspath('../../src'))

project = 'My Package'
author = 'Your Name'

extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.napoleon',  # 支持 Google/NumPy 风格
    'sphinx_autodoc_typehints',
]

# 使用 Google 风格
napoleon_google_docstring = True
napoleon_numpy_docstring = False
# docs/source/index.rst
.. My Package documentation

Welcome to My Package's documentation!
======================================

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   installation
   quickstart
   api

API Reference
=============

.. automodule:: mypackage
   :members:
   :undoc-members:
   :show-inheritance:

6.3 MkDocs(推荐)

# 安装
pip install mkdocs mkdocs-material mkdocstrings[python]

# 初始化
mkdocs new docs
# mkdocs.yml
site_name: My Package
theme:
  name: material
  language: zh
  features:
    - navigation.tabs
    - search.suggest

plugins:
  - search
  - mkdocstrings:
      handlers:
        python:
          options:
            docstring_style: google

nav:
  - 首页: index.md
  - 安装: installation.md
  - 快速开始: quickstart.md
  - API:
    - 核心模块: api/core.md
    - 工具模块: api/utils.md
# 本地预览
mkdocs serve

# 构建静态站点
mkdocs build

# 部署到 GitHub Pages
mkdocs gh-deploy

7. 内联文档工具

7.1 doctest

def factorial(n: int) -> int:
    """计算阶乘。
    
    >>> factorial(0)
    1
    >>> factorial(1)
    1
    >>> factorial(5)
    120
    >>> factorial(-1)
    Traceback (most recent call last):
        ...
    ValueError: n 必须为非负整数
    """
    if n < 0:
        raise ValueError("n 必须为非负整数")
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# 运行 doctest
# python -m doctest -v script.py
# 或在 pytest 中
# pytest --doctest-modules

7.2 __init__.py 文档

"""My Package.

一个简洁的 Python 工具包,提供 XXX 功能。

基本用法:
    >>> from mypackage import Tool
    >>> tool = Tool()
    >>> tool.run()

主要组件:
    - Tool: 主工具类
    - Config: 配置管理
    - helpers: 辅助函数
"""

from .tool import Tool
from .config import Config

__version__ = "1.0.0"
__author__ = "Your Name"
__all__ = ["Tool", "Config"]

8. 文档检查工具

# pydocstyle: 检查文档字符串规范
pip install pydocstyle
pydocstyle src/

# Ruff 也能检查文档
ruff check --select D src/

# 配置(pyproject.toml)
[tool.ruff.lint.pydocstyle]
convention = "google"  # 或 "numpy"

9. 文档编写原则

好的文档应该:

1. 解释 Why,不只是 What
   ✗ "此函数返回列表"
   ✓ "返回排序后的列表,因为前端需要有序展示"

2. 保持与代码同步
   - 修改代码时同步更新文档
   - 过时文档比没有文档更危险

3. 给出示例
   - 一个好例子胜过千言万语
   - 示例应该是可运行的

4. 面向读者
   - API 文档面向使用者
   - 内部注释面向维护者
   - README 面向新人

5. 简洁明了
   - 避免"显然"的描述
   - 删除多余的文字

10. 小结

文档类型 工具/格式 用途
Docstring Google/NumPy 风格 函数/类文档
注释 # 代码内解释
README Markdown 项目入口
API 文档 pdoc/Sphinx/MkDocs 自动生成
变更日志 CHANGELOG.md 版本记录
doctest >>> 示例 可测试文档

11. 练习题

  1. 为你之前写的模块编写完整的 Google 风格 docstring。
  2. 使用 MkDocs Material 为项目生成文档网站。
  3. 为一个函数编写 doctest,并用 pytest 运行验证。
  4. 审查你项目中的注释,清理不合理的注释并补充缺失的注释。

第九阶段(实用工具)完。接下来将进入第十阶段:综合实战。