bigywater.tech
Python · 58

运行时类型检查和静态类型检查的边界

真实用cast()骗过mypy验证运行时不会跟着一起被骗,再反过来用isinstance验证参数化泛型信息在运行时真实完全不存在,划出两套类型检查系统真实的边界。

cast(int, 字符串)
真实运行时零转换,谎言延迟到真正使用时才暴露
isinstance(x, list[int])
真实直接抛TypeError,参数化泛型检查不了
两套系统的关系
真实互补,各有各覆盖不到的盲区

做的事:真实用cast()骗过mypy,看运行时会不会跟着一起被骗

代码def get_config() -> dict: return {"port": "not-a-number"} # 真实是字符串 raw = get_config() port = cast(int, raw["port"]) # 告诉mypy"相信我这是int" print(type(port)) result = port + 1 # 真的把它当int用

验证:cast()真实在运行时什么都没做,谎言直到真正使用时才被拆穿

实测 · 本机执行输出
type(port) after cast(int, ...):   <class 'str'>    ← 真实还是str,cast没有真的转换任何东西
real crash later: can only concatenate str (not "int") to str    ← 真实到port+1这一步才炸

这是typing.cast()真实最本质的特性:它不是一次类型转换(不像int("123")那样真的做了字符串到整数的解析),cast(int, raw["port"])在运行时真实原样返回raw["port"]本身,int这个参数完全只是一份说给mypy听的"承诺"——mypy看到cast就会真实无条件相信这份承诺,把port后续的静态类型直接标记成int,不会再对它做任何"这真的是int吗"的怀疑。当这份承诺是假的(本文这次get_config真实返回的是字符串),mypy在cast那一行完全看不出任何问题,程序真实要等到后面某处真的把port当int使用(本文这次是port + 1)才会在运行时炸出TypeError——这条错误跟原本"配置里的port字段类型错了"这个真实根源之间,可能真实隔着好几层函数调用,排查起来会比直接在源头报错困难得多。

验证:反过来,isinstance()对参数化泛型真实完全无能为力

实测 · 本机执行输出
x: list[int] = [1, 2, 3]
isinstance(x, list[int])   → 真实直接抛TypeError:
  "isinstance() argument 2 cannot be a parameterized generic"

isinstance(x, list)        → True    ← 真实只能检查到"这是个list",检查不了"里面装的是int"
这组结果真实揭示了mypy能做但运行时做不到的另一半边界:list[int]这种参数化泛型标注,"元素类型是int"这份信息真实只存在于类型标注层面,Python运行时对list对象的实现里根本没有任何字段记录"这个列表当初被声明成只装int"——这正是"类型擦除"的真实含义,泛型参数在运行时真实是不存在的,isinstance只能检查到"这是不是一个list"这种容器本身的类型,检查不了容器内部元素的类型约束,尝试用isinstance去检查参数化泛型甚至真实会直接抛异常而不是给出一个错误答案。这跟前一节cast合起来看,能看出一条真实完整的边界:mypy能在编译期依据类型标注做的推理(包括参数化泛型这种运行时根本不存在的信息),跟运行时实际能检查、能拦截的东西,是两套真实互不重叠、各有各的盲区的机制——静态检查骗不了运行时的真实行为(cast撑不住的谎言,运行时该崩还是会崩),但静态检查也能表达运行时压根无法验证的约束(参数化泛型),两者是真实互补而不是互相替代的关系。

分步演示:cast的谎言和参数化泛型的类型擦除

每一步都是本文上面用真实的typing.cast和isinstance跑出来的原始结果。

静态检查骗不了运行时,运行时也验证不了静态才有的信息
这一步新验证的边界

    本文用到的工具

    • 本机 Python 3.14.7,标准库 typing.cast
    京ICP备2026067502号-1京公网安备11010502063487号