Python 中函数的注释与标注:从“写给人看”到“写给机器看”
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 机器人都会感谢你。
更多推荐
所有评论(0)