Python 类型标注入门
类型注解描述接口契约,默认不在运行时强制;配合静态检查器提升可维护性。
#concept
#growing
#dev
#python
#python
[!info] related notes
Python 类型标注入门
这个概念为什么出现
动态类型利于快写,但公共接口缺少契约时,重构与协作成本上升。类型标注让工具(Pyright/mypy)在运行前发现不匹配,同时保留运行时灵活。
[!abstract] 一句话理解 注解是可选的静态契约:
def f(x: int) -> str描述意图,解释器默认不检查,静态检查器与文档受益。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:定价函数明确入参出参,可选整数折扣百分比
# pricing_typing.py
# 业务意图:计算折后价(分)。
# 教学点:
# - 参数/返回注解;
# - X | None 允许 None,默认值决定能否省略实参;
# - 注解不阻止错误实参在运行时进入(无检查器时)。
def price_after_discount(price_cents: int, discount_percent: int | None = None) -> int:
if discount_percent is None:
return price_cents
if not 0 <= discount_percent <= 100:
raise ValueError("discount out of range")
# 本例约定整数百分比,向下取整到分;不是完整输入校验。
return price_cents * (100 - discount_percent) // 100
def main() -> None:
print(price_after_discount(1000))
print(price_after_discount(1000, 10))
if __name__ == "__main__":
main()
建议运行:
python pricing_typing.py
期望输出:
1000
900
结合场景再看三个关注点
- int | None 只描述类型,不表达 0–100 范围;百分比含义由命名/文档说明,范围由函数中的判断验证。
- 此函数检查折扣范围,但假定输入已经是正确类型的非负整分与整数百分比。注解不自动验证价格类型、范围,或排除 bool 这类 int 子类。
- 类型检查器会拒绝 price_after_discount(“1000”);裸 Python 在此路径直接返回字符串 “1000”,连返回类型注解也不会强制执行。
核心概念与准确模型
| 写法 | 含义 |
|---|---|
list[str] | 字符串列表(3.9+ 内置泛型) |
dict[str, int] | 映射 |
X | Y | 联合(3.10+) |
Optional[X] | X | None |
TypeVar/Generic | 泛型库 API |
Callable[[int], str] | 可调用 |
- 注解求值随版本而异:3.13 及以前通常在定义时求值,future annotations 可保存为字符串;3.14 默认延迟求值。求值时间改变不等于运行时校验。
- typing.cast(T, value) 原样返回 value,只向静态检查器声明类型,不转换、不检查。
- assert 可帮助静态收窄且默认会运行检查,但 python -O 可移除;不应用作必须执行的外部输入校验。Pydantic 等校验库另有运行时解析和约束规则。
边界情况与反直觉行为
- 注解可写错而程序仍运行。
- 字符串前向引用与延迟注解。
- 动态构造难被静态模型覆盖。
常见误区
[!warning] 常见误区:有注解就不需要测试 错误理解:类型代替行为验证。
正确模型:类型抓结构,测试抓行为。
工程实践
- 公共 API 先标满
- CI 跑 pyright/mypy
- 渐进标注老代码
本节总结
类型标注是工程加速器,不是第二套运行时。与检查器一起用才完整。
自测题
- 注解会改变运行时分发吗?
int | None表达什么?
参考答案
- 默认不会。
- 允许 int 或 None;不表示该参数可省略,也不规定 None 的业务含义。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| typing | 文档 | 类型系统库 |
| PEP 484 | PEP | Type Hints |