ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ty 类型系统之联合类型(Union Types):简化策略与测试套件深度解析

ty 类型系统之联合类型(Union Types):简化策略与测试套件深度解析 ty 类型系统之联合类型Union Types简化策略与测试套件深度解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本篇文章以 Ruff 仓库中crates/ty_python_semantic/resources/mdtest/union_types.md测试文档为主线系统讲解 ty 静态类型检查器中联合类型A | B的基础性质与化简simplification策略并结合ty_python_semantic的源码实现说明每个化简规则背后的集合论模型与性能考量。读完本文你将理解 ty 中联合类型的构建与规范化流程掌握reveal_type、static_assert、is_equivalent_to等测试断言的使用方式并能看懂 mdtest 测试套件的编写范式。背景ty 的集合论类型系统ty 是 Ruff 仓库中面向 Python 的类型检查器type checker实现其核心代码位于crates/ty_python_semantic。与许多传统类型检查器采用树形结构 子类型规则不同ty 将类型建模为集合论set-theoretic类型每个类型都是一个值的集合联合类型A | B对应集合的并集交集类型A B对应集合的交集否定类型~A对应补集。这套模型的基础设施集中在 crates/ty_python_semantic/src/types/set_theoretic.rs 与 crates/ty_python_semantic/src/types/set_theoretic/builder.rs。联合类型的核心数据结构是UnionType见 set_theoretic.rs#[salsa::interned(debug, heap_sizeruff_memory_usage::heap_size)] pub struct UnionTypedb { /// The union type includes values in any of these types. pub elements: Box[Typedb], /// Whether the value pointed to by this type is recursively defined. pub(crate) recursively_defined: RecursivelyDefined, }UnionType通过 Salsa 进行 intern驻留保证相同的元素组合对应同一个类型实例这对后续基于 hash 的快速等价判断至关重要。而真正负责构建并即时化简的是UnionBuilder它位于 builder.rs用户代码中任何一个int | str注解、Union[int, str]调用最终都会经由它生成规范化的Type。测试套件的地基reveal_type与 mdtest 框架union_types.md中的每个代码块都依赖两个核心测试机制reveal_type(x)断言在任意代码位置调用reveal_type可窥视该位置表达式的类型其行为记录在 directives/reveal_type.md 中。测试文档约定用# revealed: ...注释标注期望结果例如reveal_type(u1) # revealed: int | str。reveal_type可以在未导入的情况下直接使用此时会给出undefined-reveal警告并建议导入也可以从typing/typing_extensions导入。static_assert与is_equivalent_to用于在类型层面做编译期断言例如static_assert(is_equivalent_to(SA, Literal[] | AlwaysTruthy))。其中Intersection、Not、AlwaysTruthy、AlwaysFalsy、static_assert等均来自仓库内部的ty_extensions模块详见 ty_extensions.md这些特殊形式special forms用于表达 Python 语法暂时无法直接书写的类型级操作。mdtest 文档还可以通过[environment]TOML 配置块指定测试环境例如[environment] python-version 3.12该配置按标题层级继承与覆盖规则见 mdtest_config.md。union_types.md中的AlwaysTruthy/AlwaysFalsy、泛型容器等章节均要求 Python 3.12PEP 695 语法环境。联合类型的基础化简规则1. 基础联合与字面量合并最基本的联合构造示例from typing import Literal def _(u1: int | str, u2: Literal[0] | Literal[1], u3: type[int] | type[str]) - None: reveal_type(u1) # revealed: int | str reveal_type(u2) # revealed: Literal[0, 1] reveal_type(u3) # revealed: type[int | str]这里展示了三个关键行为int | str保持原样输出按规范顺序展示为int | strLiteral[0] | Literal[1]被合并为Literal[0, 1]即多个同种类字面量会被折叠为一个多值字面量类型type[int] | type[str]被化简为type[int | str]因为type[...]是协变容器其联合可以下沉到类型参数内部这一行为与后文泛型容器的联合一节的协变化简规则一致。2. 重复元素折叠def _(u1: int | int | str, u2: int | str | int) - None: reveal_type(u1) # revealed: int | str reveal_type(u2) # revealed: int | str集合论中并集是幂等的A ∪ A A因此重复元素被立即删除。从源码看builder.rs 明确声明了UnionBuilder维护的核心不变量invariants同一类型在联合中绝不出现两次。集合的并集操作天然满足该性质。3.Never与NoReturn被移除Never是没有任何值inhabitant的空集NoReturn与它等价。将空集并入任何集合都不会改变结果所以from typing_extensions import Never, NoReturn def never(u1: int | Never, u2: int | Never | str) - None: reveal_type(u1) # revealed: int reveal_type(u2) # revealed: int | str def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) - None: reveal_type(u1) # revealed: int reveal_type(u2) # revealed: int | str这一化简是急切eagerly执行的。对应到源码层面UnionBuilder::build()在最终输出时若联合中只剩一个元素则直接返回该元素本身见 builder.rs零个元素则返回Type::Never这与单元素联合应规约为被包含类型的不变量互相印证。4.object吞并一切object是所有类型的父类型即一切类型的超集。因此任何包含object的联合都可化简为objectfrom typing_extensions import Never, Any def _( u1: int | object, u2: object | int, u3: Any | object, u4: object | Any, u5: object | Never, u6: Never | object, u7: int | str | object | bytes | Any, ) - None: reveal_type(u1) # revealed: object reveal_type(u2) # revealed: object reveal_type(u3) # revealed: object reveal_type(u4) # revealed: object reveal_type(u5) # revealed: object reveal_type(u6) # revealed: object reveal_type(u7) # revealed: object注意Any也被object吞并。源码中 collapse_to_object 方法专门实现此逻辑清空元素后仅保留Type::object()单元素随后build()因单元素联合直接返回object。5. 嵌套联合的扁平化Flatteningfrom typing import Literal def _( u1: (int | str) | bytes, u2: int | (str | bytes), u3: int | (str | (bytes | bytearray)), ) - None: reveal_type(u1) # revealed: int | str | bytes reveal_type(u2) # revealed: int | str | bytes reveal_type(u3) # revealed: int | str | bytes | bytearray并集满足结合律嵌套联合被扁平化为单层。源码层面当UnionBuilder添加一个已经是Type::Union的元素时会递归展开其内部元素再逐一并入见 builder.rs这正是注释中Disjunctive normal form (DNF)联合不能包含联合这一不变式的实现。6. 基于子类型关系的化简当S是T的子类型时S | T可化简为T子类型是子集并入超集无意义from typing_extensions import Literal, LiteralString def _( u1: str | LiteralString, u2: LiteralString | str, u3: Literal[a] | str | LiteralString, u4: str | bytes | LiteralString ) - None: reveal_type(u1) # revealed: str reveal_type(u2) # revealed: str reveal_type(u3) # revealed: str reveal_type(u4) # revealed: str | bytes这里LiteralString和Literal[a]都是str的子类型因此被str吞并而bytes与str互不兼容故u4保留str | bytes。这也对应 builder.rs 中声明的规则联合中不能有任何一个类型是另一个类型的子类型直接消除子类型。字面量类型的专门处理布尔字面量Literal[True] | Literal[False]等于boolfrom typing import Literal def _( u1: Literal[True, False], u2: bool | Literal[True], u3: Literal[True] | bool, u4: Literal[True] | Literal[True, 17], u5: Literal[True, False, True, 17], ) - None: reveal_type(u1) # revealed: bool reveal_type(u2) # revealed: bool reveal_type(u3) # revealed: bool reveal_type(u4) # revealed: Literal[True, 17] reveal_type(u5) # revealed: bool | Literal[17]Literal[True, False]恰好穷尽bool的两个取值因此整体升格为boolbool | Literal[True]中Literal[True]是bool的子类型被吞并Literal[True] | Literal[True, 17]中重复的Literal[True]被折叠剩下Literal[True, 17]Literal[True, False, True, 17]先去重为{True, False, 17}其中{True, False}升格为bool最终为bool | Literal[17]。枚举字面量from enum import Enum from typing import Literal, Any from ty_extensions import Intersection class Color(Enum): RED red GREEN green BLUE blue def _( u1: Literal[Color.RED, Color.GREEN], u2: Color | Literal[Color.RED], u3: Literal[Color.RED] | Color, u4: Literal[Color.RED] | Literal[Color.RED, Color.GREEN], u5: Literal[Color.RED, Color.GREEN, Color.BLUE], u6: Literal[Color.RED] | Literal[Color.GREEN] | Literal[Color.BLUE], ) - None: reveal_type(u1) # revealed: Literal[Color.RED, Color.GREEN] reveal_type(u2) # revealed: Color reveal_type(u3) # revealed: Color reveal_type(u4) # revealed: Literal[Color.RED, Color.GREEN] reveal_type(u5) # revealed: Color reveal_type(u6) # revealed: Color枚举字面量的化简规则与布尔字面量完全同构同类枚举字面量合并u1、u4、u6单个枚举成员字面量是枚举类型Color的子类型被Color吞并u2、u3当字面量穷尽枚举的所有成员时u5覆盖RED/GREEN/BLUEu6逐个列出全部成员整体升格为Color。后半段还展示了与Intersection的组合def _( u1: Intersection[Literal[Color.RED], Any] | Literal[Color.RED], u2: Literal[Color.RED] | Intersection[Literal[Color.RED], Any], ): reveal_type(u1) # revealed: Literal[Color.RED] reveal_type(u2) # revealed: Literal[Color.RED]Intersection[Literal[Color.RED], Any]即Literal[Color.RED] Any会被化简为与Literal[Color.RED]等价的形式因此与Literal[Color.RED]的联合直接折叠为Literal[Color.RED]。其底层逻辑位于 generic_gradual_intersections.rs专门处理字面量与Any等渐进类型gradual type相交的化简。渐进类型Gradual TypesUnknown的保留与折叠Unknown是 ty 内部的渐进类型等价于大多数检查器的Any的未知形态表示类型未知但未显式标注。它不能被当作任意类型的子类型而随意消除from ty_extensions._internal import Unknown def _(u1: Unknown | str, u2: str | Unknown) - None: reveal_type(u1) # revealed: Unknown | str reveal_type(u2) # revealed: str | UnknownUnknown | str不会被化简为str——因为Unknown可能代表任何类型包括str之外的类型贸然消除会丢失信息。但多个Unknown之间仍是冗余的并集幂等def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) - None: reveal_type(u1) # revealed: Unknown | str reveal_type(u2) # revealed: Unknown | str reveal_type(u3) # revealed: str | Unknown子类型化简在Unknown存在时依然生效但Unknown本身不会被普通类型吞并def _(u1: int | Unknown | bool) - None: reveal_type(u1) # revealed: int | Unknownbool是int的子类型被消除Unknown保留结果仍是int | Unknown输出顺序取决于元素的内部排序。交集类型的联合Intersection[P, Q] | Intersection[Q, P]联合中还可以包含交集类型且交集元素的无序性交换律会被利用来化简from ty_extensions import Intersection, Not class P: ... class Q: ... def _( i1: Intersection[P, Q] | Intersection[P, Q], i2: Intersection[P, Q] | Intersection[Q, P], ) - None: reveal_type(i1) # revealed: P Q reveal_type(i2) # revealed: P QIntersection[P, Q]即P Q。两个相同或元素顺序不同但集合等价的交集在联合中重复出现时被折叠为一个P Q。这对应IntersectionBuilder维护的DNF 范式联合中是交集的并以及同一类型含交集在联合中不重复的不变量。真值性字面量AlwaysTruthy与AlwaysFalsy的特殊化简AlwaysTruthy与AlwaysFalsy是 ty 中表示必定为真/必定为假的特殊类型常与真值收窄truthiness narrowing配合使用。当它们与字面量联合时会触发一种剔除冗余字面量的化简[environment] python-version 3.12from typing import Literal, Union from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert from ty_extensions._internal import is_equivalent_to type strings Literal[foo, ] type ints Literal[0, 1] type bytes Literal[bfoo, b] def _( strings_or_truthy: strings | AlwaysTruthy, truthy_or_strings: AlwaysTruthy | strings, strings_or_falsy: strings | AlwaysFalsy, falsy_or_strings: AlwaysFalsy | strings, ... ): reveal_type(strings_or_truthy) # revealed: Literal[] | AlwaysTruthy reveal_type(truthy_or_strings) # revealed: AlwaysTruthy | Literal[] reveal_type(strings_or_falsy) # revealed: Literal[foo] | AlwaysFalsy reveal_type(falsy_or_strings) # revealed: AlwaysFalsy | Literal[foo]推导逻辑如下strings Literal[foo, ]中foo为真值、为假值。strings | AlwaysTruthyfoo已经包含在AlwaysTruthy的值域中必然为真的字符串都是真值属于冗余故化简为Literal[] | AlwaysTruthystrings | AlwaysFalsy对称地被AlwaysFalsy覆盖化简为Literal[foo] | AlwaysFalsy整数ints Literal[0, 1]与bytes Literal[bfoo, b]同理0、b为假值1、bfoo为真值。文档还用static_assert验证了组合场景type SA Union[Literal[], AlwaysTruthy, Literal[foo]] static_assert(is_equivalent_to(SA, Literal[] | AlwaysTruthy)) type SD Union[Literal[], AlwaysTruthy, Literal[foo], AlwaysFalsy, AlwaysTruthy, int] static_assert(is_equivalent_to(SD, AlwaysTruthy | AlwaysFalsy | int))SA中foo被AlwaysTruthy吸收SD中、foo分别被AlwaysTruthy/AlwaysFalsy吸收重复的AlwaysTruthy折叠int保留最终为AlwaysTruthy | AlwaysFalsy | int。从源码看builder.rs 中split_truthiness_guarded_intersection与merge_truthiness_guarded_pair这两个函数专门处理真值守卫truthiness guard的提取与合并即A ~AlwaysTruthy、A ~AlwaysFalsy这类带守卫交集的核心提取逻辑本节的化简正是这类机制的组成部分。泛型容器的联合协变、逆变与不变协变Covariant容器类型参数联合下沉[environment] python-version 3.12from typing import Any class Covariant[T]: def get(self) - T: raise NotImplementedError class Contravariant[T]: def receive(self, input: T) - None: ... class Invariant[T]: mutable_attribute: T def _( c: Covariant[Any] | Covariant[Any | str], d: Covariant[Any | str] | Covariant[Any], e: Contravariant[Any | str] | Contravariant[Any], f: Contravariant[Any] | Contravariant[Any | str], g: Invariant[Any] | Invariant[Any | str], h: Invariant[Any | str] | Invariant[Any], ): reveal_type(c) # revealed: Covariant[Any | str] reveal_type(d) # revealed: Covariant[Any | str] reveal_type(e) # revealed: Contravariant[Any] reveal_type(f) # revealed: Contravariant[Any] reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str] reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any]协变容器Covariant[Any] | Covariant[Any | str]中由于Any是Any | str的子类型集合包含关系在协变位置Covariant[Any]是Covariant[Any | str]的子类型被后者吞并输出Covariant[Any | str]逆变容器Contravariant的包含关系反转Contravariant[Any | str]反而是Contravariant[Any]的子类型故两个方向都化简为Contravariant[Any]不变容器Invariant[Any]与Invariant[Any | str]互不为子类型联合保持为两个成员的并。类型别名不改变化简结果type GradualAlias Any | str type NestedGradualAlias GradualAlias def gradual_aliases( direct_first: Covariant[Any] | Covariant[GradualAlias], direct_last: Covariant[GradualAlias] | Covariant[Any], nested_first: Covariant[Any] | Covariant[NestedGradualAlias], nested_last: Covariant[NestedGradualAlias] | Covariant[Any], ) - None: reveal_type(direct_first) # revealed: Covariant[GradualAlias] reveal_type(direct_last) # revealed: Covariant[GradualAlias] reveal_type(nested_first) # revealed: Covariant[NestedGradualAlias] reveal_type(nested_last) # revealed: Covariant[NestedGradualAlias]类型别名type alias不会让渐进类型参数变静态GradualAlias Any | str依旧携带渐进性无论直接书写还是通过一层乃至多层别名NestedGradualAlias引用协变联合的化简行为完全一致。有界泛型与元组底部实现必须保留渐进元素位置最后一段最精妙涉及有界泛型bounded generic的元组参数在底部物化bottom materialization时对渐进元素位置的保留from ty_extensions import Bottom, Top, static_assert from ty_extensions._internal import is_equivalent_to type L tuple[Any, int] type R tuple[int, Any] class C[T: tuple[int, int]]: def get(self) - T: raise NotImplementedError static_assert(is_equivalent_to(Top[C[L]], Top[C[R]])) static_assert(not is_equivalent_to(Bottom[C[L]], Bottom[C[R]])) static_assert(not is_equivalent_to(C[L], C[R])) static_assert(not is_equivalent_to(C[L] | C[R], C[L])) static_assert(not is_equivalent_to(C[R] | C[L], C[R]))要点L tuple[Any, int]与R tuple[int, Any]在Top顶部物化下等价但在Bottom底部物化下不等价——因为底部物化必须精确保留渐进元素位于元组的哪个位置。正因如此C[L]与C[R]本身不等价C[L] | C[R]也不能化简为C[L]或C[R]两个方向均被否定。这提示联合化简必须尊重泛型参数位置的语义不能在顶层形态一致时贸然合并。元组联合固定长度与可变长度的归约方向联合中同时出现定长元组与变长元组时化简方向是固定归约到可变而非相反from typing import Any def f( a: tuple[()] | tuple[int, ...], b: tuple[int, ...] | tuple[()], c: tuple[int] | tuple[str, ...], d: tuple[str, ...] | tuple[int], e: tuple[()] | tuple[Any, ...], f: tuple[Any, ...] | tuple[()], g: tuple[Any, ...] | tuple[Any | str, ...], h: tuple[Any | str, ...] | tuple[Any, ...], ): reveal_type(a) # revealed: tuple[int, ...] reveal_type(b) # revealed: tuple[int, ...] reveal_type(c) # revealed: tuple[int] | tuple[str, ...] reveal_type(d) # revealed: tuple[str, ...] | tuple[int] reveal_type(e) # revealed: tuple[Any, ...] reveal_type(f) # revealed: tuple[Any, ...] reveal_type(g) # revealed: tuple[Any | str, ...] reveal_type(h) # revealed: tuple[Any | str, ...]规则tuple[()] | tuple[Any, ...]必须化简为tuple[Any, ...]而非tuple[()]。原因在于tuple[()]空元组是tuple[int, ...]任意长度 int 元组的子集空元组是任意变长元组的特殊情况应被变长形式吸收。c/d中tuple[int]仅一个 int 的定长元组与tuple[str, ...]互不为子集联合保持原样。g/h中tuple[Any, ...]是tuple[Any | str, ...]的子类型Any被Any | str包含被后者吸收。综合理解UnionBuilder 的性能设计理解了全部化简规则后再回看 builder.rs 的Performance注释就能明白 ty 的工程权衡实际中联合有两种形态由普通用户类型类等组成的较小联合以及由字面量组成的大联合大型枚举、字符串/整数/字节字面量且会因字面量算术或字符串拼接操作而增长。对普通联合直接在向量中存储成员类型并做 O(n²) 冗余检查最有效率但字面量联合可能增长到使该做法成为性能瓶颈。为此UnionBuilder将字面量类型分组存放。由于每个不同的字符串字面量类型恰好共享相同的可能超类型集合且彼此互不为子类型除非完全相同可以避免大量不必要的冗余检查。因此UnionBuilder内部使用UnionElement将同类字面量分组IntLiterals、StringLiterals、BytesLiterals、EnumLiteralsbuild()时再展开为LiteralValueType列表见 builder.rs。这正是文档中字面量合并类化简高效执行的前提。同时set_theoretic.rs 提供的from_elements/from_two_elements工厂方法都会经由UnionBuilder完成规范化其中from_two_elements针对二元联合做了性能优化建议二元场景优先使用。如何运行与验证本套件union_types.md属于 ty 的 mdtest 测试套件其目录结构resources/mdtest按主题划分了annotations/、binary/、narrow/、comparison/等子目录覆盖联合类型在各语法场景下的行为如 annotations/union.md 专门验证typing.Union[...]与|运算符的等价性、赋值兼容性与Union[()]化简为Never等边界情况。在仓库根目录可通过cargo test运行包含的测试mdtest 测试入口位于 crates/ty_mdtest/src/lib.rs相关断言定义见 crates/ty_python_semantic/src/types/equality.rs 中的is_equivalent_to实现。如果你要扩展或验证新的化简规则推荐的实践是先在 mdtest 文档中新增一个## 章节用reveal_type的# revealed:注释写出期望输出涉及等价性时使用static_assert(is_equivalent_to(...))需要依赖 Python 版本特性时用[environment]块声明python-version。这种文档即测试的模式让化简语义的变更具备完整的可回归验证能力。小结union_types.md系统验证了 ty 联合类型的十余类化简策略化简场景规则文档章节字面量合并Literal[0] | Literal[1]→Literal[0, 1]Basic unions重复元素int | int | str→int | strDuplicate elements are collapsed空类型int | Never→intNeveris removed顶层类型int | object→objectobjectsubsumes everything嵌套联合(int | str) | bytes→int | str | bytesFlattening of nested unions子类型吸收str | LiteralString→strSimplification using subtyping布尔字面量Literal[True, False]→boolBoolean literals枚举字面量穷尽成员时升格为枚举类型Enum literals渐进类型Unknown不消除多Unknown折叠Unknown相关三节交集联合相同交集折叠Union of intersections真值字面量真/假值字面量被AlwaysTruthy/AlwaysFalsy吸收Unions withAlwaysTruthyandAlwaysFalsy泛型容器协变/逆变/不变分别处理Unions of other generic containers元组联合定长被变长吸收Unions of tuples这些规则背后统一由UnionBuilder的不变量体系支撑无单元素联合、无重复类型、DNF 扁平化、子类型消除。理解这套机制不仅能读懂 ty 的类型推导输出也能为自定义类型化简逻辑的设计提供参考。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进