文档字符串与注解
好的函数不仅需要正确实现功能,还需要清晰的接口说明。Python 提供了两种元数据机制:文档字符串(docstring)描述函数的用途和用法,函数注解(annotation)标注参数和返回值的预期类型。两者都是可选的,但合理使用能显著提升代码的可维护性。
文档字符串规范
函数体内的第一个语句如果是字符串字面量,它就会被识别为文档字符串,存储在函数对象的 __doc__ 属性中:
def greet(name):
"""向指定用户问好。
参数:
name (str): 用户的名字。
返回:
str: 问候语。
"""
return f"你好,{name}!"
print(greet.__doc__)
PEP 257 对文档字符串的格式有明确约定:
- 第一行是简短摘要,说明对象用途。不要在这里写对象名或类型(可通过其他方式获取),以大写字母开头,以句点结尾。
- 多行文档字符串的第二行应为空行,在视觉上将摘要与详细描述分开。
- 后续行描述调用约定、参数、返回值、副作用、异常等。
def calculate_area(length, width):
"""计算矩形的面积。
根据给定的长和宽计算矩形面积。如果输入为负数,
返回 0 而不是抛出异常。
参数:
length (float): 矩形的长度,必须为非负数。
width (float): 矩形的宽度,必须为非负数。
返回:
float: 矩形的面积。如果输入为负数则返回 0。
"""
if length < 0 or width < 0:
return 0
return length * width
doc 属性的访问
文档字符串可以通过 __doc__ 属性或 help() 函数查看:
print(greet.__doc__) # 直接打印原始字符串
help(greet) # 格式化输出,包含签名和文档字符串
在交互式解释器和 IDE 中,help() 是快速查阅函数说明的标准方式。第三方工具如 Sphinx 可以从文档字符串自动生成 HTML 文档。
缩进处理
Python 解析器不会删除多行字符串字面量中的缩进,因此文档字符串中保留了函数体内的缩进空格。文档处理工具(如 help())会按约定自动去除缩进:以文档字符串第一行之后的第一个非空行确定基准缩进量,然后删除所有行开头处等价的空白。
def example():
"""第一行摘要。
这是详细描述。
缩进与函数体一致。
"""
pass
print(example.__doc__)
# 第一行摘要。
#
# 这是详细描述。
# 缩进与函数体一致。
注意第一行通常与引号相邻,其缩进在字符串中不明显,因此不能用第一行确定缩进量。
函数注解语法
函数注解是可选的元数据,以字典形式存放在函数的 __annotations__ 属性中。注解对函数执行没有任何影响,Python 解释器不会检查或强制类型。
参数注解写在形参名后加冒号:
def greet(name: str, times: int = 1) -> str:
return (f"你好,{name}!\n") * times
print(greet.__annotations__)
# {'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
返回值注解写在参数列表和冒号之间,用 -> 表示:
def add(a: int, b: int) -> int:
return a + b
注解不强制检查
注解只是提示,Python 运行时完全忽略它们。传入"错误"类型的数据不会报错:
def add(a: int, b: int) -> int:
return a + b
print(add("hello, ", "world")) # "hello, world",完全合法
print(add(1.5, 2.5)) # 4.0,也合法
如果需要类型检查,应使用外部工具如 mypy、Pyright 或 IDE 的静态分析功能。这些工具在开发时检查类型一致性,而不影响运行时性能。
复杂注解
注解可以是任何表达式,不限于内置类型。从 Python 3.9 开始,内置集合类型(list、dict、set 等)支持泛型语法:
def process(items: list[int]) -> dict[str, int]:
return {str(x): x for x in items}
print(process([1, 2, 3]))
# {'1': 1, '2': 2, '3': 3}
对于可选值、联合类型等复杂场景,可以使用 typing 模块或 | 语法(Python 3.10+):
def find(items: list[str], target: str) -> int | None:
try:
return items.index(target)
except ValueError:
return None
print(find(["a", "b", "c"], "b")) # 1
print(find(["a", "b", "c"], "z")) # None
Python 3.12 泛型语法(PEP 695)
Python 3.12 引入了更简洁的泛型函数语法,用 def[T] 声明类型参数:
def get_first[T](items: list[T]) -> T | None:
return items[0] if items else None
print(get_first([1, 2, 3])) # 1
print(get_first(["a", "b"])) # "a"
旧写法(3.11 及之前)需要显式导入 TypeVar:
from typing import TypeVar, List, Optional
T = TypeVar('T')
def get_first(items: List[T]) -> Optional[T]:
return items[0] if items else None
新写法减少了样板代码,但只在 Python 3.12+ 可用。
常见错误
把注解当作运行时类型检查:
# 错误认知:以为注解会阻止错误类型传入
# 实际上 Python 完全忽略注解
注解表达式在定义时求值,如果引用了尚未定义的名称会报错:
# def broken(x: MyClass) -> MyClass: # NameError: name 'MyClass' is not defined
# pass
# 解决方案:用字符串延迟求值(前向引用)
def fixed(x: "MyClass") -> "MyClass":
pass
class MyClass:
pass
从 Python 3.7+ 开始,可以通过 from __future__ import annotations 让所有注解自动变为字符串形式,解决前向引用问题。
小结
文档字符串通过 __doc__ 提供运行时可查的说明,遵循 PEP 257 的格式约定。函数注解通过 __annotations__ 提供类型元数据,但 Python 不强制检查,需配合静态分析工具使用。两者都是可选的,但在团队协作和大型项目中,它们是代码自文档化的重要手段。