1.引言:为什么“注释”与“标注”不是同义词

在 Python 世界里,“注释”(comment) 与“标注”(annotation) 常被混为一谈:二者都写在源码里,都以 :# 开头,甚至都能提升代码可读性。然而它们服务的对象完全不同:

注释写给“人”,解释“为什么”而非“做什么”。

标注写给“机器”,让静态分析器、IDE、框架在不执行代码的情况下理解类型契约。
本文将系统梳理两条技术脉络,并给出大量实战片段,帮助你在 2000 行以上的项目里同时获得“人类可读”与“机器可验证”的双重收益。

2.历史回顾

1991:Python 0.9.1 只有 # 行注释。

2001:PEP 257 引入文档字符串(docstring) 规范。

2006:PEP 3107 在 Py3.0 新增函数注解语法,但未规定语义。

2014:PEP 484 以“类型提示”(type hints) 名义复活注解,伴随 mypy 0.1。

2020:PEP 604 允许 X | Y 替代 Union[X, Y]

2022:PEP 698 @override 让继承体系更安全。

2025:PEP 747 正在草案阶段,引入“泛型默认值”。

3.注释(Comments)的艺术

3.1 三种形态

# 行内注释:仅解释紧接的表达式
offset = 8  # 单位:字节,用于网络对齐

# 块注释:描述一段逻辑
# -----------------------------------------------------------
# 根据 RFC 6455 第 5.2 节对帧进行掩码处理
# 1. 生成 4 字节 masking key
# 2. 按位异或 payload
# -----------------------------------------------------------

def mask_payload(payload: bytes, key: bytes) -> bytes:
    """
    文档字符串(docstring):可被 help()、Sphinx、IPython 提取
    参数
    ----
    payload : bytes
        原始 WebSocket 帧负载
    key : bytes
        4 字节掩码
    返回
    ----
    bytes
        掩码后的负载
    """
    ...

3.2 文档字符串风格

reST/Sphinx 风格(官方推荐)

Google 风格(TensorFlow、Kubernetes 采用)

NumPy 风格(SciPy、pandas 采用)

下面以 Google 风格为例:

def fetch_json(url: str, *, timeout: float = 5) -> dict[str, Any]:
    """Fetches JSON from a given URL.

    Args:
        url: Target endpoint.
        timeout: Seconds before giving up.

    Returns:
        A decoded JSON dictionary.

    Raises:
        requests.HTTPError: When status code != 200.
        json.JSONDecodeError: When response body is invalid.
    """

3.3 注释误区

描述“显而易见”的代码:i += 1 # 递增 i

过期注释:重构后忘记更新。

用注释“隐藏”坏味道:与其写 # 这里很复杂,不如用提炼函数。

4.标注(Annotations)的体系

4.1 语法速览

from typing import List, Dict, Any, Callable

Processor = Callable[[str], Dict[str, Any]]

def parse_csv(
    file_path: str,
    *,
    delimiter: str = ",",
    encoding: str = "utf-8"
) -> List[Dict[str, Any]]:
    ...

函数签名中,file_path: str 是参数标注,-> List[Dict[str, Any]] 是返回标注。

4.2 typing 模块核心工具

  • 基本构造:Union, Optional, Literal, Final

  • 泛型:TypeVar, Generic, ParamSpec, TypeVarTuple

  • 结构子类型:Protocol 替代 Java 式接口

  • 异步世界:Awaitable, Coroutine, AsyncGenerator

示例:泛型工厂

from typing import TypeVar, Generic, Callable

T = TypeVar("T")

class Pool(Generic[T]):
    def __init__(self, factory: Callable[[], T]) -> None:
        self._factory = factory
        self._items: list[T] = []

    def acquire(self) -> T:
        return self._items.pop() if self._items else self._factory()

    def release(self, item: T) -> None:
        self._items.append(item)

4.3 三重世界

  • 运行时:解释器完全忽略标注,除非你用 __annotations__ 访问。

  • 静态检查:mypy/pyright 在 CI 中跑,无性能开销。

  • IDE:PyCharm、VS Code 基于类型做补全、跳转、重构。

5.进阶主题

5.1 ParamSpec 与回调

from typing import ParamSpec, TypeVar, Callable, Any

P = ParamSpec("P")
T = TypeVar("T")

def timed(func: Callable[P, T]) -> Callable[P, T]:
    import time
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        t0 = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print("elapsed", time.perf_counter() - t0)
    return wrapper

5.2 dataclasses 与 TypedDict

from dataclasses import dataclass
from typing import TypedDict

@dataclass(slots=True)
class User:
    id: int
    name: str
    active: bool = True

class UserJSON(TypedDict):
    id: int
    name: str
    active: bool

 5.3 装饰器与标注

import functools
from typing import Callable, TypeVar

F = TypeVar("F", bound=Callable[..., Any])

def debug(func: F) -> F:
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper  # type: ignore[return]

使用 functools.wraps 保留原函数的 __annotations__,但 mypy 仍需 type: ignore 来消除协变返回错误。

5.4 运行时校验

  • beartype:零成本断言,装饰器即可

  • typeguard:支持详尽错误信息

  • pydantic:结合 BaseModel 做解析 + 校验

from beartype import beartype

@beartype
def repeat(text: str, count: int) -> list[str]:
    return [text] * count

repeat("hi", "3")  # 立刻抛 TypeHintError

6.工程化落地

 6.1 CI 配置示例(GitHub Actions)

- name: Type check with mypy
  run: |
    pip install mypy pydantic
    mypy --strict src/
- name: Lint with ruff
  run: |
    pip install ruff
    ruff check .

 6.2 Sphinx 自动生成

# docs/conf.py
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.napoleon",  # 支持 Google/NumPy 风格
]

# 在 .rst 中
.. autofunction:: mypkg.core.calc_hash

6.3 兼容性策略

  • 渐进式:用 from __future__ import annotations 把标注推迟到运行时解析,兼容旧解释器。

  • Stub 文件:为 C 扩展或旧代码提供 module.pyi 侧写。

7.未来展望

  • PEP 649:在 3.14 中默认启用延迟求值标注,解决前向引用性能问题。

  • PEP 747:允许 class Box[T=int] 类似默认值,简化泛型容器。

  • 宏类型(Macro Types):社区草案,尝试把 Decimal(18,2) 这类数据库精度信息带进类型系统。

 8.结语

写注释是为了,写标注是为了工具。二者并非取代,而是互补:

  • 注释回答“为什么”,在业务上下文消失前保留知识;

  • 标注回答“是什么”,让机器在毫秒级检查亿级路径。
    当你下次写函数时,请同时按下“#”和“:”键——
    五年后的自己、同事、CI 机器人都会感谢你。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐