用 typing 明确函数边界:类型提示与运行时校验
用 typing 明确函数边界
Python 的类型提示可以让编辑器和静态检查工具理解函数约定,但它不会自动拒绝错误参数。例如,标注为 int 的参数在运行时仍可能收到字符串。可靠的边界函数通常需要同时完成两件事:用 typing 描述接口,用普通 Python 代码验证真实输入。
边界内外使用不同类型
来自 JSON、配置文件或消息队列的数据尚未可信,适合标注为 Mapping[str, object];通过验证后,再转换为字段明确的领域对象。下面的示例要求 Python 3.10 或更高版本,仅使用标准库。
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from typing import Literal, Mapping, TypeAlias, cast
Status: TypeAlias = Literal["pending", "paid"]
ALLOWED_STATUS = {"pending", "paid"}
@dataclass(frozen=True)
class Order:
order_id: int
amount: Decimal
status: Status
def __post_init__(self) -> None:
# bool 是 int 的子类,因此这里使用精确类型判断。
if type(self.order_id) is not int:
raise TypeError("order_id 必须是 int,且不能是 bool")
if not self.amount.is_finite() or self.amount < 0:
raise ValueError("amount 必须是有限且非负的数")
if self.status not in ALLOWED_STATUS:
raise ValueError("status 只能是 pending 或 paid")
def parse_order(raw: Mapping[str, object]) -> Order:
"""把未经信任的映射转换为已校验的 Order。"""
if not all(type(key) is str for key in raw):
raise TypeError("所有字段名都必须是 str")
required = {"order_id", "amount", "status"}
actual = set(raw)
missing = sorted(required - actual)
extra = sorted(actual - required)
if missing:
raise ValueError(f"缺少字段: {', '.join(missing)}")
if extra:
raise ValueError(f"未知字段: {', '.join(extra)}")
order_id = raw["order_id"]
if type(order_id) is not int:
raise TypeError("order_id 必须是 int,且不能是 bool")
amount_text = raw["amount"]
if type(amount_text) is not str:
raise TypeError("amount 必须是十进制字符串")
try:
amount = Decimal(amount_text)
except InvalidOperation as exc:
raise ValueError("amount 不是有效的十进制数") from exc
status_value = raw["status"]
if type(status_value) is not str or status_value not in ALLOWED_STATUS:
raise ValueError("status 只能是 pending 或 paid")
# cast 只向静态检查工具说明类型,不执行运行时转换或校验。
status = cast(Status, status_value)
return Order(order_id=order_id, amount=amount, status=status)
samples: list[dict[str, object]] = [
{"order_id": 7, "amount": "19.90", "status": "paid"},
{"order_id": True, "amount": "3", "status": "pending"},
{"order_id": 8, "amount": "NaN", "status": "paid"},
{"order_id": 9, "amount": "5", "status": "paid", "note": "x"},
]
for number, sample in enumerate(samples, 1):
try:
print(number, parse_order(sample))
except (TypeError, ValueError) as exc:
print(f"{number} {type(exc).__name__}: {exc}")
运行后应得到类似输出:
1 Order(order_id=7, amount=Decimal('19.90'), status='paid')
2 TypeError: order_id 必须是 int,且不能是 bool
3 ValueError: amount 必须是有限且非负的数
4 ValueError: 未知字段: note
关键边界条件
bool 是 int 的子类,所以 isinstance(True, int) 为真;若业务不接受布尔值,应使用 type(value) is int。Decimal("NaN") 可以构造成功,却不是有限金额,因此还要调用 is_finite()。字符串本身也是序列,验证列表类参数时不能仅依赖“可迭代”。示例还采用严格字段策略:缺失字段和额外字段都会报错;如果协议允许扩展字段,可以移除额外字段检查。
Order.__post_init__ 防止调用者绕过解析函数直接创建明显非法对象,但类型标注本身仍不负责校验。typing.cast、Literal 和返回值标注也都不会改变运行时对象。若需要通用校验框架,可以通过 inspect.signature() 和 typing.get_type_hints()读取标注,但联合类型、泛型及递归结构会显著增加实现复杂度;对小型稳定接口,显式校验通常更清楚。
适用范围
这种模式适合处理配置、反序列化结果、命令行参数及其他不可信输入。对于完全由同一模块生成的内部对象,可减少重复校验,把检查集中在最外层入口,以避免不必要的运行时成本。
参考资料:Python 标准库文档中的“typing——类型提示支持”“dataclasses——数据类”“decimal——十进制定点和浮点运算”;可在 Python 文档目录依次进入“库参考”后按模块名查阅。
本文由 AI 辅助编写,示例需要读者在自己的 Python 环境中自行验证。
- 点赞
- 收藏
- 关注作者
评论(0)