Chapter 51: Typing and Runtime Boundary
Python 的类型标注把“程序员希望对象满足什么形状”写进源码,CPython runtime 仍按对象、名字绑定、属性查找、调用协议和异常路径执行代码。本章要建立的判断能力是:看到 typing、TypedDict、Protocol、泛型别名和 annotation 时,能够区分哪些结论来自静态类型检查器,哪些状态真实存在于运行时对象上,哪些校验需要工程代码显式承担。
这个边界会直接影响框架设计。CLI 参数、HTTP payload、插件接口、ORM model、序列化层和配置系统都可能使用类型标注表达契约。类型检查器能在提交前发现一批调用错误,runtime 只会在执行到真实对象操作时暴露缺字段、缺方法、值类型错误或协议不匹配。
本章使用一个小型加载场景贯穿说明。它包含 Protocol 表达能力、TypedDict 表达字典结构、Generic 表达容器内部元素类型,并在最后用 runtime 校验收束边界。
from typing import Generic, Protocol, TypeVar, TypedDict, cast, runtime_checkable
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
class Payload(TypedDict):
id: int
name: str
tags: list[str]
class Reader(Protocol):
def read(self, size: int) -> bytes:
...
@runtime_checkable
class Closable(Protocol):
def close(self) -> None:
...
def load(reader: Reader, raw: dict[str, object]) -> Box[Payload]:
data = cast(Payload, raw)
return Box(data)
这段代码里有三类信息。第一类是 runtime 真实创建的对象,例如 Box 类对象、Payload 类型对象、Reader 类型对象、函数对象和 annotation 存储。第二类是静态工具使用的关系,例如 T 的一致性、Reader 的结构化能力、Payload 的键和值类型。第三类是工程边界,例如 cast(Payload, raw) 改变类型检查器的视角,传入的 raw 对象仍是普通 dict。Python 3.14 文档明确说明 runtime 不会强制执行函数和变量的类型标注,typing 模块提供的是 type hints 的 runtime 支撑对象和词汇表,类型结论通常由 type checker、IDE 和 linter 使用;这一点是整章的基础边界,参见 Python 3.14 typing 文档。
51.1 typing, Generic, TypeVar, and Protocol
typing 是 Python 类型系统的标准词汇层。它提供 TypeVar、Generic、Protocol、TypedDict、cast、get_type_hints 等对象,让源码可以表达“这个函数接收什么类型关系”“这个对象需要有什么方法”“这个容器内部元素应保持什么一致性”。这些对象的主要消费者是静态类型检查器,CPython runtime 只负责创建、保存和暴露相关对象。
TypeVar 表达类型变量。类型变量的作用是把多个位置绑定成同一个类型选择,例如 Box[T] 中的 T 同时约束构造参数、实例属性和返回位置。它解决的问题是“一处传入 str 后,相关位置也应是 str;一处传入 Payload 后,相关位置也应是 Payload”。
Generic 表达一个类可以被类型参数化。Python 3.12 起支持类型参数语法,例如 class Box[T]: ...;兼容旧版本时仍常见 T = TypeVar("T") 与 class Box(Generic[T]): ...。Python 文档说明泛型类会提供 __class_getitem__(),因此 Box[int] 这类表达式能在 runtime 被求值。这个求值结果服务 annotation 和 introspection,实例对象本身仍按普通类实例运行。
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
number_box = Box[int]("runtime accepts this value")
print(type(number_box.value))
类型检查器会把 Box[int]("runtime accepts this value") 判为不一致,因为构造参数位置期望 int。CPython 执行这段代码时会调用 Box.__init__,把字符串对象绑定到 self.value。这里的 runtime 路径是普通函数调用和属性写入,T 没有进入对象字段校验路径。
Protocol 表达结构化类型关系。结构化类型关系的工作定义是:一个对象只要提供了目标能力,就可以被视为满足某个协议。Reader 协议要求对象有 read(size: int) -> bytes。类型检查器会检查传入对象是否具备兼容的 read 方法,runtime 调用时只会执行 reader.read(size);如果对象没有 read 属性,错误会在属性查找或调用时出现。
from typing import Protocol
class Reader(Protocol):
def read(self, size: int) -> bytes:
...
class MemoryReader:
def read(self, size: int) -> bytes:
return b"x" * size
def take(reader: Reader) -> bytes:
return reader.read(3)
print(take(MemoryReader()))
MemoryReader 没有显式继承 Reader,类型检查器仍可根据结构接受它。runtime 侧看到的是一个普通实例,函数体里的关键动作是属性查找 reader.read 和方法调用。这个例子把协议边界固定下来:静态层判断“对象形状是否满足协议”,执行层只处理真实属性和真实调用。
@runtime_checkable 会给 Protocol 增加有限的 runtime 查询能力。它允许 isinstance(obj, SomeProtocol) 这种检查,但检查内容是属性存在性,类型签名和属性值类型不会被完整验证。Python 3.12 以后,runtime-checkable protocol 的 isinstance 检查使用 inspect.getattr_static() 查找属性,并且 protocol 成员在类创建后被视为固定成员;这些版本细节来自 typing 文档中 runtime_checkable 的说明。
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None:
...
class Resource:
close = "not a method"
print(isinstance(Resource(), Closable))
这个检查可以返回 True,因为对象上存在名为 close 的属性。随后执行 Resource().close() 会因为字符串对象无法调用而失败。@runtime_checkable 适合做轻量能力探测,工程代码如果依赖方法可调用性、参数形状、返回值类型和副作用约束,就需要继续写显式校验或用测试覆盖真实调用路径。
在源码阅读时,可以按三个层级定位 typing 抽象:先看 annotation 里出现了哪些类型词汇,再看这些词汇对应的 runtime 对象是否会参与执行,最后看函数体里真实发生的属性查找、下标访问、调用和异常路径。大多数泛型、TypeVar 和 Protocol 关系停留在 annotation 与工具层;对象行为仍由实例、类、descriptor、slot 和普通方法调用决定。
51.2 TypedDict and runtime data shape
TypedDict 用来表达字典的结构化数据形状。它的工作定义是:用类声明语法写出一组键及其值类型,让类型检查器把普通 dict 按结构化 payload 处理。它解决的是“字典字段缺失、字段拼写错误、字段值类型不一致”这类静态可发现问题。
在 runtime 中,TypedDict 实例就是普通 dict。Python 文档明确说明 TypedDict instances are simply dicts,并且键集合和值类型期望只由类型检查器强制执行,参见 TypedDict 文档。这使它适合表达跨函数、跨模块、跨接口的字典契约,但它本身不承担输入数据验证。
from typing import NotRequired, Required, TypedDict
class Payload(TypedDict):
id: int
name: str
tags: list[str]
class PartialPayload(TypedDict, total=False):
id: Required[int]
name: str
tags: NotRequired[list[str]]
payload: Payload = {"id": "wrong", "name": "book", "tags": [1, 2]}
print(type(payload))
print(payload["id"])
类型检查器会指出 id 和 tags 的值类型与 Payload 声明不一致。CPython runtime 会创建一个字典对象,把字符串和整数列表照常存入。随后 payload["id"] 返回字符串对象。失败何时出现取决于后续代码对这个值做什么:如果后续执行整数运算,错误会在运算协议或函数调用中出现;如果后续只是序列化为 JSON,它可能继续流动到更远的边界。
Required、NotRequired 和 total=False 影响静态键存在性判断,也会在 TypedDict 类型对象上留下可自省元数据。常见属性包括 __required_keys__、__optional_keys__ 和 __total__。这些属性能帮助文档生成器、schema 生成器或校验器读取声明,但读取声明与验证真实数据是两个阶段。
from typing import NotRequired, Required, TypedDict
class PartialPayload(TypedDict, total=False):
id: Required[int]
name: str
tags: NotRequired[list[str]]
print(PartialPayload.__required_keys__)
print(PartialPayload.__optional_keys__)
print(PartialPayload.__total__)
这段代码读取的是 PartialPayload 类型对象上的元数据。它说明声明中哪些键被视为必需,哪些键被视为可选。它没有扫描某个具体 dict,也没有验证每个值的类型。工程上要把 TypedDict 变成 runtime 保证,需要额外写边界函数:先检查对象是不是 dict,再检查必需键,接着检查值类型,最后返回经过校验的对象或构造新的 domain object。
from typing import TypedDict
class Payload(TypedDict):
id: int
name: str
tags: list[str]
def parse_payload(raw: object) -> Payload:
if not isinstance(raw, dict):
raise TypeError("payload must be a dict")
if not isinstance(raw.get("id"), int):
raise TypeError("payload.id must be int")
if not isinstance(raw.get("name"), str):
raise TypeError("payload.name must be str")
tags = raw.get("tags")
if not isinstance(tags, list) or not all(isinstance(item, str) for item in tags):
raise TypeError("payload.tags must be list[str]")
return {"id": raw["id"], "name": raw["name"], "tags": tags}
这个函数把 TypedDict 的静态形状转成 runtime 检查。函数的返回值仍是 dict,但调用方可以把 parse_payload 视为信任边界:它接收未知 object,经过键和值检查后,返回满足 Payload 契约的数据。TypedDict 适合放在边界函数的返回 annotation 上,输入位置更适合写 object 或 dict[str, object],这样类型标注能表达“外部输入尚未被信任”。
TypedDict 与 class model 的关系也要固定。class Payload(TypedDict): ... 看起来像定义类,runtime 创建的是一个用于类型系统和元数据承载的特殊类对象;调用 Payload(id=1, name="x", tags=[]) 会得到普通字典。继承 TypedDict 可以合并字段,泛型 TypedDict 可以表达字段类型随类型参数变化。它们都服务数据形状声明,真实数据仍通过 dict 的哈希表、键查找、下标访问和修改规则运行。
排查数据形状问题时,先问三个问题:传入数据是否经过了 runtime 解析函数;类型检查器是否能看到 TypedDict annotation;后续代码是否把某个字段当成更具体类型使用。第一个问题决定运行安全,第二个问题决定提交前反馈,第三个问题决定错误触发点。
51.3 Static vs runtime typing
静态类型和 runtime 类型处在两个执行阶段。静态类型检查器读取源码、annotation、类型 stub、配置和控制流,输出诊断结论。CPython runtime 加载模块、创建对象、执行字节码、查找属性、调用函数、传播异常。两者使用同一份源码,但它们读取的信息、运行时机和失败表现不同。
下面的代码把这个差异压缩到一个例子里:
from typing import TypedDict, cast
class Payload(TypedDict):
id: int
name: str
raw: dict[str, object] = {"id": "100", "name": "book"}
payload = cast(Payload, raw)
print(payload["id"] + 1)
cast(Payload, raw) 会告诉类型检查器把 raw 视为 Payload。在 runtime 中,typing.cast 返回第二个参数本身,字典对象没有被复制,也没有被扫描。最后一行执行字符串与整数相加,错误来自二元运算协议。Python 文档对 cast() 的描述也是这个边界:它向 type checker 发出指定类型信号,runtime 返回原值且不执行检查,参见 typing.cast 文档。
assert_type、reveal_type、assert_never 等工具也要放回阶段边界。assert_type(value, Type) 要求类型检查器确认推断结果,runtime 返回第一个参数并且没有副作用。assert_never(value) 表达静态不可达分支,runtime 真执行到它时会抛异常。它们能帮助维护静态控制流结论,但它们无法替代输入解析、权限检查、长度限制或资源关闭。
类型检查器也会受可见性影响。它能分析源码中可见的 annotation、分支、赋值和导入;它对反射、动态属性注入、setattr、运行时生成类、条件导入、插件注册和 monkey patch 的理解会受配置和工具能力限制。runtime 对这些动态行为天然执行,因为执行路径上对象真实存在。工程设计要让关键契约尽量写在静态工具能读取的位置,例如公开函数签名、Protocol、TypedDict、dataclass 字段和明确返回值。
runtime 类型则来自对象本身。type(obj) 读取对象的类型指针,isinstance(obj, cls) 沿类型层级、ABC hook 或 protocol runtime check 执行,属性访问进入 __getattribute__、descriptor 和实例字典路径。annotation 可以参与工具提示和框架逻辑,但普通 Python 运算不会自动读取 annotation 后再决定是否执行。
from typing import Any
value: int = "42"
unknown: Any = value
print(type(value))
print(unknown.upper())
value: int = "42" 对类型检查器是诊断点,对 CPython 是名字绑定。unknown: Any 会在静态层放宽后续检查,runtime 中 unknown 仍指向同一个字符串对象。Any 的作用是改变类型检查器的约束强度,它不会改变对象身份、对象类型或方法集合。
把静态结论迁移到 runtime 时,需要固定判断顺序。第一步,确认数据来源是否可信:内部构造值、外部请求、文件反序列化、环境变量和插件返回值的信任等级不同。第二步,确认 annotation 的消费者:类型检查器、文档生成器、框架 introspection 或 runtime validator。第三步,确认真实执行动作:属性查找、下标访问、方法调用、算术运算、序列化或 I/O。第四步,在边界位置写校验函数,并让校验函数返回更窄的静态类型。这样静态工具和 runtime 行为会形成闭环。
静态类型的价值在大型代码库里尤其明显。它能把许多接口不一致提前到编辑器、CI 和 review 阶段。runtime 校验的价值在信任边界上尤其明显。它能防止外部输入、版本漂移、跨语言数据和插件行为把错误对象带入核心逻辑。二者要按阶段组合:静态类型负责设计期反馈,runtime 校验负责执行期防线。
51.4 Annotation evaluation and generic alias
annotation 是函数、类和模块对象上的元数据。它可以表达类型,也可以被框架当成 schema、依赖注入、序列化或文档生成材料。理解 annotation 的关键是求值时机:同一段 annotation 表达式,在不同 Python 版本和 future import 下,可能在定义时求值、以字符串保存,或延迟到访问时求值。
Python 3 的 annotation 语义经历了三个主要模型。Python 3.0 到 3.13 的默认 stock semantics 会在定义处急切求值。Python 3.7 起可使用 from __future__ import annotations 把 annotation 保存为字符串。Python 3.14 起默认使用 deferred evaluation,annotation 表达式延迟到访问时求值;annotationlib 模块成为处理 annotation introspection 的低层入口。这个版本边界来自 annotationlib 文档的 Annotation semantics。
class Node:
child: "Node | None"
print(Node.__annotations__)
在字符串化模型中,annotation 字典中可能保存字符串。使用 annotation 的框架若直接读取 __annotations__,会遇到字符串、真实类型、ForwardRef 或延迟求值产生的对象。Python 3.14 的 annotationlib.get_annotations() 支持以 VALUE、FORWARDREF、STRING 等格式取回 annotation。VALUE 尝试求值并返回对象,遇到未定义名字可能抛错;FORWARDREF 允许未解析名字以 forward reference 形式存在;STRING 更适合文档展示。文档同时提醒大多数 annotation introspection 功能可能执行任意代码,参见 annotationlib 模块说明。
from annotationlib import Format, get_annotations
class Node:
child: Node | None
print(get_annotations(Node, format=Format.STRING))
这段 Python 3.14 示例强调格式选择。文档生成器通常更适合 STRING,因为它关心展示;依赖注入容器若需要真实类型对象,可能选择 VALUE,并承担求值失败和执行任意代码的安全边界;schema 生成器遇到前向引用时,可以选择 FORWARDREF,再分阶段解析。
Python 3.10 到 3.13 中,访问 annotation 的推荐入口通常是 inspect.get_annotations();Python 3.14 起,annotationlib.get_annotations() supersedes inspect.get_annotations()。旧版本直接读取类的 __annotations__ 还有继承相关差异。官方 Annotations Best Practices 明确把版本分开说明,这对框架代码很关键:一个库若承诺支持 Python 3.10 到 3.14,就要把 annotation 读取封装在兼容层里。
generic alias 是另一个 runtime 可观察对象。list[int]、dict[str, int] 和 tuple[str, ...] 这类表达式会在 runtime 生成参数化泛型对象。对于内置容器,list[int] 的类型是 types.GenericAlias;它有 __origin__、__args__ 和 __parameters__ 等只读属性。Python 标准类型文档说明 GenericAlias 主要用于 type annotations,并且 isinstance([1, 2], list[str]) 会抛 TypeError,参见 Generic Alias Type 文档。
alias = dict[str, list[int]]
print(alias.__origin__)
print(alias.__args__)
print(type(alias))
print(alias({"a": [1, 2], "b": ["x"]}))
alias.__origin__ 指向未参数化的 dict,alias.__args__ 保存参数。调用 alias(...) 会构造普通字典,元素类型不会被扫描。文档还说明参数化泛型在对象创建时会擦除类型参数,list[str]() 的结果类型仍是 list。这就是泛型别名的 runtime 边界:参数信息存在于 alias 对象上,容器实例没有自动携带逐元素类型约束。
annotation evaluation 和 generic alias 的共同点是“类型信息成为 runtime 可观察对象”。差异在于使用方式。annotation 挂在函数、类和模块上,需要按照版本语义和求值格式读取;generic alias 是表达式求值结果,可以直接检查 __origin__ 和 __args__。两者都适合给工具、框架和文档系统提供材料;两者都不自动变成普通运算的类型守卫。
51.5 Runtime typing cost and boundary
runtime typing 的成本来自检查动作本身。检查一个普通 isinstance(x, int) 成本很低;递归检查 dict[str, list[Payload]] 需要遍历键、值、列表元素和嵌套字典;检查 Protocol 的属性存在性需要属性查找;解析 annotation 可能触发求值、导入、前向引用处理和异常路径。成本来源要落到对象分配、遍历深度、属性查找、函数调用和可能的用户代码执行上。
工程上要把类型信息分成三个使用层级。第一层是静态契约,例如函数签名、Protocol、TypedDict、泛型和类型别名。这一层服务 IDE、CI、review 和维护者理解,runtime 成本接近零。第二层是边界校验,例如解析 HTTP JSON、读取配置文件、反序列化消息、加载插件返回值。这一层需要显式检查,成本换取信任边界清晰。第三层是深度 runtime 类型系统,例如递归 validator、schema 框架、数据模型库和依赖注入容器。这一层功能更强,代价是额外遍历、错误聚合、转换规则、版本兼容和安全边界。
贯穿例子里的 load 函数可以改成两段式结构:外层解析未知输入,内层使用已经收窄的 Payload。这种结构让成本集中在入口,核心逻辑按可信对象执行。
from typing import Protocol, TypedDict
class Payload(TypedDict):
id: int
name: str
tags: list[str]
class Reader(Protocol):
def read(self, size: int) -> bytes:
...
def parse_payload(raw: object) -> Payload:
if not isinstance(raw, dict):
raise TypeError("payload must be a dict")
if not isinstance(raw.get("id"), int):
raise TypeError("payload.id must be int")
if not isinstance(raw.get("name"), str):
raise TypeError("payload.name must be str")
tags = raw.get("tags")
if not isinstance(tags, list) or not all(isinstance(item, str) for item in tags):
raise TypeError("payload.tags must be list[str]")
return {"id": raw["id"], "name": raw["name"], "tags": tags}
def handle_payload(payload: Payload) -> str:
return f"{payload['id']}:{payload['name']}:{len(payload['tags'])}"
parse_payload 是 runtime 边界,承担外部输入检查。handle_payload 是内部逻辑,依赖静态类型表达前置条件。这样排查错误时能快速定位:如果外部数据不满足结构,错误来自解析边界;如果内部逻辑对字段使用错误,类型检查器和测试应在更早阶段暴露。
何时使用 runtime 校验,可以按数据来源和失败成本判断。外部输入、持久化文件、网络消息、跨进程边界、插件返回值、反射构造对象和安全敏感参数需要 runtime 校验。内部私有函数、局部变量、中间迭代器、纯计算辅助函数和已经由入口校验过的对象,通常依赖静态类型、单元测试和代码审查即可。这个判断的核心不是“是否有 annotation”,而是“错误对象从哪里进入系统,失败后代价在哪里发生”。
何时依赖静态类型,也要看反馈链路。团队有类型检查 CI、编辑器能读取配置、依赖库提供 stub、动态行为被限制在小范围内时,静态类型能稳定降低接口漂移。项目大量使用动态属性注入、运行时生成函数、反射插件和字符串协议时,静态类型仍可覆盖核心接口,但要把动态部分包进窄接口,并在边界上用 runtime 校验补足。
runtime typing 还涉及安全边界。读取 annotation 可能执行表达式或触发导入;递归 validator 可能遍历攻击者控制的大对象;错误消息可能泄露内部结构;自动转换可能把坏数据转换成看似合法的对象。框架设计中,annotation introspection 要明确执行环境,输入校验要限制深度、大小和转换规则,错误返回要区分内部日志与对外消息。
本章最终的判断顺序可以压缩为五步。先看 annotation 表达的是名义类型、结构类型、字典形状、泛型关系还是元数据。再看它的消费者是 type checker、IDE、文档工具、框架 introspection 还是 runtime validator。接着看 runtime 对象上真实存在什么:类、实例、dict、GenericAlias、annotation dict 或延迟求值函数。随后看错误对象可能从哪里进入系统。最后决定成本放置位置:设计期用静态类型,信任边界用显式校验,深层动态框架用受控的 introspection。
最小自检任务
阅读下面代码,判断每一行的 runtime 行为和类型检查器可能给出的结论。重点说明 Payload、Packet[int]、cast、@runtime_checkable 分别改变了哪一层状态。
from typing import Generic, Protocol, TypeVar, TypedDict, cast, runtime_checkable
T = TypeVar("T")
class Packet(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
class Payload(TypedDict):
id: int
tags: list[str]
@runtime_checkable
class HasClose(Protocol):
def close(self) -> None:
...
class Strange:
close = "closed"
raw: dict[str, object] = {"id": "7", "tags": [1, 2]}
payload = cast(Payload, raw)
packet = Packet[int](payload["id"])
print(type(payload))
print(type(packet.item))
print(isinstance(Strange(), HasClose))
答案要点
Payload声明的是字典形状契约,runtime 中payload仍指向raw那个普通dict对象,因此type(payload)输出dict。cast(Payload, raw)改变类型检查器对raw的视角,runtime 返回原对象;它没有检查id是否为int,也没有检查tags是否为list[str]。- 类型检查器可能在
raw字面量赋给dict[str, object]时接受它,因为每个值都可作为object;在cast之后,类型检查器会把payload["id"]视为int。 Packet[int](payload["id"])在静态层看起来满足Packet[int],因为payload["id"]被视为int;runtime 中payload["id"]实际是字符串对象,packet.item保存字符串。type(packet.item)输出str,说明泛型参数没有进入实例字段校验路径。HasClose使用@runtime_checkable后可以参与isinstance,检查关注close属性存在性;Strange.close是字符串属性,存在性检查可以通过。- 后续若执行
Strange().close(),错误会来自字符串不可调用;这属于真实调用路径错误,超出@runtime_checkable的轻量能力探测范围。 - 稳定做法是把
raw先交给解析函数检查键和值类型,再把返回值标注为Payload,核心逻辑依赖静态类型表达已校验前提。
本章知识点总结
- 类型边界:Python 类型标注主要服务静态工具,CPython runtime 仍按对象、绑定、查找、调用和异常路径执行。
- TypeVar 关系:
TypeVar把多个 annotation 位置绑定成同一个类型选择,runtime 不据此扫描实例字段。 - 泛型对象:
Generic让类可以参数化,Box[int]等表达式可在 runtime 求值,但实例仍按普通类对象运行。 - Protocol 结构:
Protocol表达对象需要具备的能力,静态检查关注结构兼容,runtime 调用关注真实属性和方法。 - runtime_checkable:
@runtime_checkable允许协议进入isinstance,检查重点是属性存在性,签名和值类型需要额外验证。 - TypedDict 形状:
TypedDict表达字典键和值类型,具体实例在 runtime 中仍是普通dict。 - 键元数据:
__required_keys__、__optional_keys__和__total__可用于读取TypedDict声明,真实数据验证需要单独执行。 - cast 边界:
typing.cast改变静态视角,runtime 返回原对象,适合配合已经完成的外部校验使用。 - annotation 版本:annotation 求值模型受 Python 版本和 future import 影响,Python 3.14 起默认延迟求值并提供
annotationlib。 - GenericAlias:
list[int]等表达式生成可自省的泛型别名,容器实例创建时不会保留逐元素类型约束。 - 成本来源:runtime 类型校验成本来自对象遍历、属性查找、函数调用、annotation 求值和可能的用户代码执行。
- 工程顺序:先识别 annotation 消费者,再定位 runtime 对象,随后确认数据来源,最后决定静态检查、边界校验和深度 introspection 的放置位置。