Ruff 类型检查器深度解析:`typing.Unpack` 在 ty 中的语义与实现 📅 发布时间:2026/9/10 0:51:34 👁 浏览次数: Ruff 类型检查器深度解析typing.Unpack在 ty 中的语义与实现【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读Unpack[Ts]是 Python 3.11 引入的 legacy 拼写形式等价于 PEP 646 中的*Ts。本文以 Ruff 仓库中 ty 类型检查器的 mdtest 规格文档 unpack.md 为主体系统讲解Unpack在泛型特化、可变参数推断、Callable 参数展开、类型别名、默认值等场景下的完整语义并深入其底层实现源码帮助你理解并掌握 variadic 泛型在类型检查器中的行为边界、错误恢复策略与诊断规则。背景Unpack[Ts]与*Ts的等价关系在 PEP 646TypeVarTuple类型变量元组之前Python 的类型语法中不存在 任意数量位置参数 的抽象。TypeVarTuple正是为此而生一个TypeVarTuple可以绑定零个或多个类型参数。而Unpack[Ts]是它的 legacy 拼写——在支持*Ts星号解包语法之前Unpack[Ts]承担了同样的职责二者语义完全等价。在 ty 的 mdtest 测试目录结构中这两种拼写分别由两份规格文档覆盖typevartuple.mdlegacy 记法下TypeVarTuple的定义与特化含*Tsunpack.md本文主体legacy 记法下Unpack的 distinct 语法路径../pep695/typevartuple.mdPEP 695 新语法下的共享语义。如文档开篇所述The shared semantics of type variable tuples are covered in../pep695/typevartuple.md; this file checks the distinct syntax paths used byUnpack——即类型变量元组的共享语义在 PEP 695 文档中覆盖本文件专门检查Unpack使用的不同语法路径。所有测试片段默认在python-version 3.11环境下运行[environment] python-version 3.11Unpack在运行时既可从typing导入也可从typing_extensions导入。这一点在源码中得到了明确印证在 special_form.rs 中SpecialFormType::Unpack的注释写着 The symboltyping.Unpack(which can also be found astyping_extensions.Unpack)。如何阅读与运行这份规格文档该文档是 ty 类型检查器的mdtest 测试规格而不是普通的说明文档。每个代码块中的reveal_type(...)调用代表类型检查器应在该处揭示出的精确类型# error: [rule-name]注释则代表预期诊断。mdtest 测试框架通过 tests/mdtest.rs 注册datatest_stable::harness! { { test mdtest, root ./resources/mdtest, pattern r\.md$ }, { test lint_doc, root ./resources/lint_docs, pattern r\.md$ }, }即resources/mdtest目录下所有.md文件都会被作为测试夹具fixture执行。运行方式为标准的 cargo 测试cargo test -p ty_python_semantic --test mdtest每个测试片段会被提取为一个独立的 Python 文件mdtest_snippet.py进行类型检查并生成快照snapshot到resources/mdtest/snapshots目录。这意味着本文档中的每一行断言都是可执行的事实这也是 ty 项目保证类型检查器行为可回归、可验证的方式。详细的测试机制说明见 ty_test/README.md。泛型特化Generic specializationUnpack可以在 legacy 泛型声明中引入一个类型变量元组也可以作为特化参数展开一个固定元组。from typing import Generic, TypeVarTuple, Unpack Ts TypeVarTuple(Ts) class Array(Generic[Unpack[Ts]]): value: tuple[Unpack[Ts]] reveal_type(Array[()]().value) # revealed: tuple[()] reveal_type(Array[int, str]().value) # revealed: tuple[int, str] reveal_type(Array[Unpack[tuple[int, str]]]().value) # revealed: tuple[int, str]三种特化方式得到一致的结果特化写法value的揭示类型Array[()]tuple[()]零个参数Array[int, str]tuple[int, str]Array[Unpack[tuple[int, str]]]tuple[int, str]第三种写法展示了一个重要特性Unpack既可以在泛型声明中引入TypeVarTuple也可以在特化时展开一个具体的元组类型。Unpack[tuple[int, str]]与直接写int, str等价。从源码实现看这正是 type_expression.rs 中SpecialFormType::Unpack分支的处理逻辑// Preserve valid unpack targets so that Unpack[...] follows the same // argument-binding path as an equivalent starred annotation. if inner_ty.exact_tuple_instance_spec(self.db()).is_some() || matches!( inner_ty, Type::TypeVar(typevar) if typevar.is_typevartuple(self.db()) ) { inner_ty }只要Unpack的操作数解析为确切的元组实例或TypeVarTuple就原样保留该类型让它走与等价的星号标注完全相同的参数绑定路径。同时每个Unpack[...]下标表达式都会被打上TypeExpressionFlags::UNPACK标志type_expression.rs供后续上下文判断使用。可变参数推断Variadic parameter inferenceUnpack应用于*args时ty 会保留位置参数的数量与类型而不是退化为tuple[Any, ...]from typing import TypeVarTuple, Unpack Ts TypeVarTuple(Ts) def collect(*args: Unpack[Ts]) - tuple[Unpack[Ts]]: reveal_type(args) # revealed: tuple[*Tscollect] raise NotImplementedError reveal_type(collect()) # revealed: tuple[()] reveal_type(collect(1, a)) # revealed: tuple[Literal[1], Literal[a]]注意两点函数体内args的类型是tuple[*Tscollect]——collect后缀表示这是绑定到collect函数作用域的TypeVarTuple而不是模块级的Ts调用点处collect(1, a)被推断为tuple[Literal[1], Literal[a]]字面量类型被保留。参数派生类型在赋值冲突中依然保留legacy 拼写还必须保证当外围赋值期望一个不兼容的返回类型时从参数派生出的类型依然保留以便后续诊断指向真正的错误来源而不是错误地抹掉推断精度inferred collect(1) reveal_type(inferred) # revealed: tuple[Literal[1]] # error: [invalid-assignment] indirect: tuple[str] inferred # error: [invalid-assignment] direct: tuple[str] collect(1)collect(1)的类型是tuple[Literal[1]]把它赋值给tuple[str]无论是通过中间变量间接赋值还是直接赋值都会触发invalid-assignment诊断同时Literal[1]的精度在推断阶段不受影响。Callable 参数展开Unpack可以将类型变量元组展开进 callable 的位置参数列表同一个元组也可以描述转发给该 callable 的参数from typing import Callable, TypeVar, TypeVarTuple, Unpack R TypeVar(R) Ts TypeVarTuple(Ts) def invoke( callback: Callable[[Unpack[Ts]], R], *args: Unpack[Ts], ) - R: raise NotImplementedError def format_value(value: int, label: str, /) - str: return f{label}: {value} reveal_type(invoke(format_value, 1, value)) # revealed: str # TODO: Validate arguments matched to the variadic parameter against the TypeVarTuple inferred # from the callback. reveal_type(invoke(format_value, 1)) # revealed: str这里invoke是一个高阶函数callback接受Unpack[Ts]展开后的位置参数*args同样是Unpack[Ts]。调用invoke(format_value, 1, value)时ty 从callback与实参同时推断Ts最终R被绑定为str。源码中紧随其后的TODO注释则诚实地标出了当前实现的已知限制尚未验证实参与从 callback 推断出的TypeVarTuple是否匹配——因此invoke(format_value, 1)参数数量不匹配目前仍被接受。这种 TODO 标注在 mdtest 规格中用于记录已知行为缺口是 ty 持续演进的一部分。通过解包的 TypeVarTuple 转发 ParamSpecParamSpec描述的是 callable 的完整参数集含关键字参数而TypeVarTuple只描述位置参数。二者可以协同工作一个转发参数规格的 callable其本身可以连同参数一起传给一个用解包 TypeVarTuple 描述位置参数的 callablefrom typing import Callable, ParamSpec, TypeVarTuple, Unpack P ParamSpec(P) Ts TypeVarTuple(Ts) def invoke(callback: Callable[[Unpack[Ts]], None], *args: Unpack[Ts]) - None: ... def forward(callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) - None: ... def one_arg(value: int) - None: ... invoke(forward, one_arg, 1)调用链分析forward是Callable[P, None]其P被one_arg(value: int) - None和实参1实例化为[int]invoke的Ts则从位置实参forward, one_arg, 1推断。整条调用链既涉及ParamSpec的实例化又涉及TypeVarTuple的解包展开ty 均能正确处理。这也印证了本文档的测试主题legacy 泛型各语法路径之间的互操作性。类型别名Type aliaseslegacy 别名可以使用Unpack[Ts]并且在特化时既可以接收逐个类型也可以接收解包的元组类型from typing import TypeVarTuple, Unpack Ts TypeVarTuple(Ts) Alias tuple[int, Unpack[Ts]] def f( fixed: Alias[str, bool], unbounded: Alias[Unpack[tuple[str, ...]]], ) - None: reveal_type(fixed) # revealed: tuple[int, str, bool] reveal_type(unbounded) # revealed: tuple[int, *tuple[str, ...]]Alias[str, bool]固定元素展开为tuple[int, str, bool]Alias[Unpack[tuple[str, ...]]]无界元组str, ...无法确定长度因此揭示为tuple[int, *tuple[str, ...]]——*解包符出现在揭示类型中表示这是一个可变长段。fixed与unbounded的揭示类型差异正是固定长度 vs 未知长度两种 TypeVarTuple 特化的典型体现。关于别名的更多边界行为如Never保留、无法拆分 TypeVarTuple 等可参阅 typevartuple.md 的 Type Aliases 一节。不支持的 Union 解包Unsupported union unpacking将 TypeVarTuple 解包进Union目前不被支持。被拒绝的联合体无论单独出现还是嵌套在另一个泛型特化中都会恢复为object运行时元素访问同样恢复为objectfrom typing import TypeVarTuple, Union, Unpack Ts TypeVarTuple(Ts) # TODO: shouldnt error # error: [invalid-type-form] def reject_union(value: Union[Unpack[Ts]]) - None: # TODO: should reveal Union[*Ts] representation reveal_type(value) # revealed: object # error: [invalid-type-form] Unpacking a TypeVarTuple in Union is not supported def reject_nested_union(value: list[Union[Unpack[Ts], None]]) - None: reveal_type(value) # revealed: list[object] def element_types(values: tuple[Unpack[Ts]]) - None: # TODO: should reveal Union[*Ts] representation reveal_type(values[0]) # revealed: object for value in values: # TODO: should reveal Union[*Ts] representation reveal_type(value) # revealed: object这里可以提炼出三条行为准则Union[Unpack[Ts]]直接触发invalid-type-form诊断源码中对应诊断消息为 Unpacking aTypeVarTupleinUnionis not supported见 type_expression.rs 附近的诊断构造嵌套形式list[Union[Unpack[Ts], None]]同样报错且外层list[...]的元素类型恢复为object对tuple[Unpack[Ts]]的元素做索引或迭代访问时当前揭示为object。代码中的多个TODO: should reveal Union[*Ts]表明ty 认为这里应该揭示出Union[*Ts]这种表示形式*Ts展开进联合体只是目前尚未实现属于已知的精度缺口而非最终设计。Union 中嵌套的无效解包操作数Unpack[int]在 Python 语法上是合法的写法可以解析、可以运行但其操作数不是元组类型。当这样的联合体出现在泛型特化内部时ty 会报告一条普通的invalid-type-form诊断from typing import Union, Unpack # error: [invalid-type-form] Unpack can only unpack a tuple type or TypeVarTuple def invalid_operand(value: list[Union[Unpack[int], None]]) - None: reveal_type(value) # revealed: list[tuple[Unknown, ...] | None]注意揭示类型中的细节无效的Unpack[int]并没有恢复为object而是恢复为tuple[Unknown, ...]未知元素的同质元组。这与源码实现完全对应——在 type_expression.rs 中当操作数既不是确切元组也不是TypeVarTuple时self.store_type_expression_flags( ast::ExprRef::from(subscript), TypeExpressionFlags::INVALID_UNPACK, ); if !inner_ty.is_unknown() let Some(builder) self.context.report_lint(INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link(builder.into_diagnostic( Unpack can only unpack a tuple type or TypeVarTuple, )); } Type::homogeneous_tuple(db, env, Type::unknown())实现细节值得注意Unpack会被打上INVALID_UNPACK标志诊断消息为 Unpackcan only unpack a tuple type orTypeVarTuple并且无论操作数是什么始终恢复为Type::homogeneous_tuple(..., Type::unknown())即tuple[Unknown, ...]。这就是测试断言中tuple[Unknown, ...] | None的来源。无效解包上下文仍会推断操作数一个重要的设计原则是无效的解包上下文不应压制其操作数的运行时错误。同时字符串注解不会执行其内容因此无效字符串注解中未解析的名字保持静默from typing import Unpack # error: [invalid-type-form] Unpack is not allowed in parameter annotations # error: [unresolved-reference] Name Missing used when not defined def invalid_context(value: Unpack[Missing]) - None: ... # error: [invalid-type-form] Unpack is not allowed in parameter annotations def invalid_stringified_context(value: Unpack[Missing]) - None: ...两条路径的差异正是是否执行写法invalid-type-form诊断unresolved-reference诊断Unpack[Missing]未字符串化有有Missing未定义Unpack[Missing]字符串化有无字符串不执行Unpack出现在普通参数注解非*args/**kwargs中本身是非法上下文报 Unpackis not allowed in parameter annotations源码消息模板为 Unpackis not allowed in {}s见 type_expression.rs其中{}由当前类型表达式上下文填充。但操作数Missing在第一种写法中仍会被求值从而暴露出未定义名字错误。源码中的相关处理逻辑type_expression.rslet inner_ty if self.in_string_annotation() (is_nested_unpack || is_nested_kwargs || is_invalid_context) { // Invalid string annotations never execute, so their operands must not // produce runtime errors even though their inferred types are still needed. let mut speculative self.speculate_without_diagnostics(); let inner_ty speculative.infer_type_expression(arguments_slice); self.extend(speculative); inner_ty } else { self.infer_type_expression(arguments_slice) };即字符串注解 嵌套/关键字/无效上下文时通过speculate_without_diagnostics()进行无诊断的推测性推断既拿到操作数的推断类型又不产生运行时错误类诊断非字符串路径则正常推断并报告诊断。此外同一段源码还实现了两种额外的非法形式嵌套 UnpackUnpack[Unpack[...]]→ Unpackcannot be nested嵌套 kwargsUnpack出现在非顶层的**kwargs注解中→ Unpackis only valid as the top-level**kwargsannotation form。这些错误路径也都会在后面的验证部分看到对应的测试断言。具体元组与嵌套元组解包Unpack可以为*args展开一个具体的元组注解包括嵌套的无界元组from typing import Unpack def accept( *args: Unpack[tuple[bool, Unpack[tuple[str, ...]], bytes]], ) - None: ... accept(True, phase, status, bok) accept(True, bok) accept(True, 1, bbad) # error: [invalid-argument-type]*args的类型规格是(bool, *str..., bytes)——即第一个参数必须是bool最后一个必须是bytes中间可以有任意数量的str。因此accept(True, phase, status, bok)合法bool 两个strbytesaccept(True, bok)合法零个str的退化情形accept(True, 1, bbad)非法中间出现int不匹配str触发invalid-argument-type。这是具体类型 嵌套 Unpack组合在可变参数上的精确应用展现了 ty 对嵌套可变长段的建模能力。默认值Defaults从 Python 3.13 起TypeVarTuple支持default参数。默认值本身可以使用Unpack显式特化会覆盖默认值[environment] python-version 3.13from typing import Generic, TypeVarTuple, Unpack Ts TypeVarTuple(Ts, defaultUnpack[tuple[int, str]]) class WithDefault(Generic[Unpack[Ts]]): value: tuple[Unpack[Ts]] reveal_type(WithDefault().value) # revealed: tuple[int, str] reveal_type(WithDefault[bool, bytes]().value) # revealed: tuple[bool, bytes]不提供类型参数时Ts落入默认值Unpack[tuple[int, str]]value为tuple[int, str]显式传入bool, bytes时默认值被覆盖value为tuple[bool, bytes]。关于默认值的补充规则记录在 typevartuple.md 中TypeVarTuple的默认值必须是解包的元组类型或另一个TypeVarTuple直接写defaulttuple[int, str]会触发invalid-legacy-type-variable诊断typing_extensions.TypeVarTuple可将default反向移植到旧版 Python。在更早的 Python 版本如 3.10上需要通过typing_extensions导入Unpack才能配合使用from typing import Generic from typing_extensions import TypeVarTuple, Unpack Ts TypeVarTuple(Ts, defaultUnpack[tuple[int, str]]) class WithBackportedDefault(Generic[Unpack[Ts]]): attr: tuple[Unpack[Ts]] reveal_type(WithBackportedDefault().attr) # revealed: tuple[int, str]验证规则ValidationUnpack要求操作数是元组类型且元组特化中只能有一个 variadic 解包。这一节集中了所有非法形式的诊断断言from typing import Generic, TypeVar, TypeVarTuple, Unpack U TypeVar(U) Ts TypeVarTuple(Ts) Xs TypeVarTuple(Xs) Ys TypeVarTuple(Ys) class Pair(Generic[Unpack[Ts], U]): ... # error: [invalid-generic-class] Only one TypeVarTuple parameter is allowed in a Generic subscription class MultipleUnpack(Generic[Unpack[Xs], Unpack[Ys]]): ... # error: [invalid-generic-class] Only one TypeVarTuple parameter is allowed in a Generic subscription class StarThenUnpack(Generic[*Xs, Unpack[Ys]]): ... # error: [invalid-generic-class] Only one TypeVarTuple parameter is allowed in a Generic subscription class UnpackThenStar(Generic[Unpack[Xs], *Ys]): ... def invalid( # error: [invalid-type-form] Unpack can only unpack a tuple type or TypeVarTuple non_tuple: Pair[Unpack[int], str], # error: [invalid-type-form] Multiple unpacked variadic tuples are not allowed in a tuple specialization multiple: tuple[Unpack[Ts], Unpack[tuple[str, ...]]], ) - None: reveal_type(non_tuple) # revealed: Pair[*tuple[Unknown, ...], str] # error: [invalid-type-form] Unpack can only unpack a tuple type or TypeVarTuple def invalid_vararg(*args: Unpack[int]) - None: reveal_type(args) # revealed: tuple[Unknown, ...] # error: [invalid-type-form] Unpack can only unpack a tuple type or TypeVarTuple def invalid_stringified_vararg(*args: Unpack[int]) - None: reveal_type(args) # revealed: tuple[Unknown, ...] # error: [invalid-type-form] Unpack cannot be nested def nested(*args: Unpack[Unpack[tuple[int, ...]]]) - None: ... # error: [invalid-type-form] Bare TypeVarTuple Ts is not valid in this context in a parameter annotation def nested_bare_typevartuple(*args: Unpack[tuple[Ts]]) - None: ...逐条解析这些规则规则一Generic订阅中只允许一个TypeVarTuple。MultipleUnpack、StarThenUnpack、UnpackThenStar三种写法分别对应两个Unpack、*Xs与Unpack[Ys]混用、Unpack[Xs]与*Ys混用都触发invalid-generic-class诊断消息统一为 Only oneTypeVarTupleparameter is allowed in aGenericsubscription。这也与 typevartuple.md 中Generic[*Xs, *Ys]的约束一致——legacy 与 PEP 695 语法在单 TypeVarTuple这一根本约束上行为相同。规则二Unpack操作数必须是元组或TypeVarTuple。Unpack[int]无论是作为Pair的特化参数Pair[Unpack[int], str]还是直接用于*args注解invalid_vararg、invalid_stringified_vararg都会触发 Unpackcan only unpack a tuple type orTypeVarTuple。注意后两者的恢复类型均为tuple[Unknown, ...]——与前面 Union 嵌套场景的恢复策略一致。规则三元组特化中不允许出现多个解包的可变长段。tuple[Unpack[Ts], Unpack[tuple[str, ...]]]触发 Multiple unpacked variadic tuples are not allowed in atuplespecialization——一个元组类型只能有一个可变长段否则元素位置无法唯一确定。规则四Unpack不可嵌套。Unpack[Unpack[tuple[int, ...]]]触发 Unpackcannot be nested对应源码中is_nested_unpack检查分支type_expression.rs。规则五裸TypeVarTuple不能出现在Unpack的元组内部。Unpack[tuple[Ts]]中Ts是裸的未解包触发 Bare TypeVarTupleTsis not valid in this context in a parameter annotation。此外non_tuple的揭示类型Pair[*tuple[Unknown, ...], str]也揭示了错误恢复的另一面无效的Unpack[int]在Pair特化内部恢复为*tuple[Unknown, ...]U固定为str整体类型仍然可读可用避免了级联错误。源码实现全景Unpack的完整检查管线综合前文各节ty 对Unpack[...]的检查管线可以归纳为以下步骤全部位于 type_expression.rs 的SpecialFormType::Unpack分支标记为下标表达式存储TypeExpressionFlags::UNPACK上下文判定读取推断标志判定三种非法情形——嵌套 UnpackIN_UNPACK_TYPE_ARGUMENT、嵌套 kwargsIN_KWARG_ANNOTATIONIN_NESTED_TYPE_EXPRESSION、无效上下文不在IN_VARARG_ANNOTATION | IN_KWARG_ANNOTATION | IN_VALID_UNPACK_CONTEXT集合内操作数推断对Unpack的操作数递归推断类型字符串注解中的无效场景使用speculate_without_diagnostics静默推断合法性检查操作数必须解析为确切元组实例或TypeVarTuple否则打上INVALID_UNPACK标志并报 Unpackcan only unpack a tuple type orTypeVarTuple恢复为tuple[Unknown, ...]传播合法的Unpack结果走与等星球号标注相同的参数绑定路径与typevartuple.md中的*Ts行为统一。正是这一管线的存在使得本文档中的每一条reveal_type断言和每一条error:诊断都有了确定的实现依据。文档中所有TODO注释如Union[*Ts]表示、参数匹配验证则标记了管线的已知边界属于 ty 的持续改进项。总结Unpack[Ts]作为*Ts的 legacy 拼写在 ty 中拥有完整的、可回归验证的语义实现。本文从规格文档出发覆盖了它的全部使用场景泛型声明与特化含具体元组展开、*args可变参数的类型精度保留、Callable 位置参数展开、与ParamSpec的协同转发、类型别名、默认值以及五大类非法形式Union 解包、非元组操作数、嵌套 Unpack、多可变长段、裸TypeVarTuple的精确诊断与错误恢复策略。若要进一步探索推荐按以下路径深入unpack.md本文的规格来源可执行断言全集typevartuple.mdTypeVarTuple的定义、特化、方差与默认值规则paramspec.mdParamSpec与P.args/P.kwargs的完整约束type_expression.rsUnpack与所有特殊形式类型的底层实现special_form.rs特殊形式符号含Unpack的运行时识别tests/mdtest.rs 与 ty_test/README.mdmdtest 规格的运行机制。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考