
Ruff 中的 ty 类型检查器使用与~运算符书写交集类型与否定类型注解【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuff 仓库中内置的 ty 类型检查器支持一种源自typing_extensions.Intersection的实验性类型注解语法直接在注解位置使用表示交集类型Intersection Type使用一元~表示否定类型Negation Type无需任何导入。本文以 intersection_types.md 这一 mdtest 规范文档为骨架结合 ty_python_semantic 的源码实现完整讲解这两种类型表达式的语义、与|联合运算符的优先级关系、在不同 Python 版本3.13 与 3.14下的求值行为、延迟注解的规避方案以及底层报错诊断的触发原理。读完本文你将能够在自己的 Python 3.14 项目或结合from __future__ import annotations的 3.13 项目中写出正确、可读且可被 ty 精确推断的交集/否定类型注解并理解其在运行时失败时的诊断行为。说明/~是 ty 实验性语法见 experimental-syntax 诊断文档当前仓库中 ty 的 README 将其定位为对类型系统前沿语法的先行实现。一、交集类型与否定类型基本语义在 Python 的类型注解体系中交集类型描述同时满足多个类型约束的值否定类型描述排除某个类型的值。传统上这两种类型需要通过typing_extensions.Intersection这类特殊形式才能表达。ty 提供了更接近数学记号的写法交集类型使用运算符否定类型使用一元~运算符且无需导入ty_extensions.Intersection。1.1 基本示例Python 3.14mdtest 文档在[environment]中指定python-version 3.14这是/~在注解位置可被直接求值的前提具体原因见下文Python 版本与求值时机一节[environment] python-version 3.14class A: ... class B: ... class C: ... def _( a_and_b: A B, i1: A B | C, i2: A | B C, not_a: ~A, nested: A B C, ) - None: reveal_type(a_and_b) # revealed: A B reveal_type(i1) # revealed: (A B) | C reveal_type(i2) # revealed: A | (B C) reveal_type(not_a) # revealed: ~A reveal_type(nested) # revealed: A B C这里reveal_type是 ty 提供的调试内建函数会在检查结果中输出对应表达式被推断出的精确类型a_and_b: A B被推断为A B交集类型i1: A B | C被推断为(A B) | C即的优先级高于|先结合成交集再做联合i2: A | B C被推断为A | (B C)同样印证优先级高于|not_a: ~A被推断为~A否定类型nested: A B C被推断为A B C说明交集运算符是左结合且支持链式多元素写法。1.2 源码印证与~如何在类型表达式求值器中落地上述行为的实现位于 crates/ty_python_semantic/src/types/infer/builder/type_expression.rs。当在注解上下文遇到二元运算符时求值器会分派不同的处理分支ast::Operator::BitAnd即先推断左右两侧操作数作为类型表达式再调用IntersectionType::from_two_elements(db, env, left_ty, right_ty)构造交集类型见该文件第 398–436 行。from_two_elements是 types.rs 中IntersectionType构造逻辑的一部分负责规范化、去重与化简。ast::UnaryOp::Invert即一元~推断操作数类型后调用operand_ty.negate(db, env)构造否定类型见该文件第 708–743 行。交集与否定类型的核心数据结构定义在 types.rs 中Type::Intersection(IntersectionTypedb)是Type枚举的一个变体第 1865 行而IntersectionType、UnionType由 set_theoretic.rs 导出types.rs表明 ty 采用集合论set-theoretic模型统一描述交集、联合与否定类型。优先级层面A B | C之所以被解析为(A B) | C是因为 Rust 侧解析器在生成 AST 时即遵循 Python 运算符优先级BitAnd高于BitOr|因此求值器看到的树形结构天然是(A B) | C随后逐层求值即可。二、与~不能用于值位置与~仅在注解上下文type expression中才被 ty 解释为交集/否定类型。如果把它们用在普通表达式值位置Python 解释器会按位运算语义执行__and__/__invert__魔术方法而普通类通常未实现这些方法从而在运行时抛出TypeError# error: [unsupported-operator] Operator is not supported between objects of type class A and class B Invalid1 A B # error: [unsupported-operator] Unary operator ~ is not supported for object of type class A Invalid2 ~A2.1 源码印证unsupported-operator诊断的产生该诊断由 crates/ty_python_semantic/src/types/diagnostic.rs 声明其规则说明见 resources/lint_docs/unsupported-operator.md。在类型表达式求值器中处理的分支在!ignore_runtime_errors(self)时会先以推测式speculative方式把操作数当作值来推断并调用Type::try_call_bin_op(..., ast::Operator::BitAnd, ...)试探运行时二元运算是否可行失败则调用report_unsupported_binary_operation上报unsupported-operator诊断type_expression.rs。类似地~分支会尝试调用__invert__魔术方法失败时通过report_unsupported_unary_operator上报同文件第 721–740 行。换句话说即使代码位置错误ty 仍然会继续读懂这个注解它照常构造出交集/否定类型参与后续类型推断同时给出unsupported-operator诊断提醒你运行时必然失败。文档注释同文件第 409–411 行明确指出这是为了报告运行时操作实际使用的操作数类型。三、Python 3.13急切求值导致运行时错误3.1 问题的根源在 Python 3.13 及更早版本中函数注解默认是急切求值eager evaluationdef f(a: A B)中的A B会在定义函数时立即作为普通表达式求值而普通类A、B没有实现__and__/__invert__于是直接抛出TypeError。ty 的策略是报出诊断但照常把注解解释为交集/否定类型。mdtest 文档为此提供了专用用例[environment] python-version 3.13class A: ... class B: ... def _( # error: [unsupported-operator] Operator is not supported between objects of type class A and class B a_and_b: A B, # error: [unsupported-operator] Unary operator ~ is not supported for object of type class A not_a: ~A, ) - None: reveal_type(a_and_b) # revealed: A B reveal_type(not_a) # revealed: ~A注意这里的组合行为诊断unsupported-operator照常出现因为 Python 3.13 下注解表达式会被急切求值运行时必然失败但reveal_type的输出依然是A B与~A证明 ty 的静态类型推断不受影响——它知道你想表达的是交集/否定类型。这正是静态检查与运行时行为解耦的典型体现ty 忠于类型语义完成推断同时忠实反映运行时的真实结果。3.2 源码印证ignore_runtime_errors与版本敏感行为回顾 type_expression.rs 中的if !ignore_runtime_errors(self)判断当该函数返回 true 时即处于忽略运行时错误的求值模式典型场景是解析字符串化/延迟注解ty 不再尝试推测操作数的运行时值也就不会上报unsupported-operator。这直接对应文档第三部分延迟注解的行为差异急切求值3.13 默认ignore_runtime_errors为 false → 尝试值推断 → 发现__and__/__invert__不存在 → 报unsupported-operator延迟求值进入字符串注解/from __future__ import annotations路径 →ignore_runtime_errors为 true → 跳过运行时检查 → 只按类型语义求值。从源码结构看该开关正是 ty 区分注解会被运行时执行与注解仅作为字符串被静态解析两种场景的关键机制。四、在 Python 3.13 上使用延迟注解Deferred Annotations既然问题出在急切求值解决方案就是把注解的求值推迟到运行时不再执行。标准做法是使用from __future__ import annotations让注解全部变成字符串[environment] python-version 3.13from __future__ import annotations class A: ... class B: ... def _(a_and_b: A B, not_a: ~A) - None: reveal_type(a_and_b) # revealed: A B reveal_type(not_a) # revealed: ~A加入 future import 后A B不再在函数定义时求值运行时错误消失unsupported-operator诊断也不再出现——但 ty 依然通过解析字符串形式的注解得到与 3.14 完全一致的推断结果A B与~A。4.1 字符串化注解Stringified annotations同样延迟求值from __future__ import annotations的本质就是把注解字符串化因此手动给注解加引号也能达到同样效果def _(a_and_b: A B, not_a: ~A) - None: reveal_type(a_and_b) # revealed: A B reveal_type(not_a) # revealed: ~A两种方式殊途同归A B与~A作为字符串字面量运行时求值不会触发任何运算符ty 则会解析字符串内容并构造出交集/否定类型。4.2 源码印证字符串注解的解析路径在 type_expression.rs 中可以看到多处if !self.in_string_annotation()守卫当处于字符串注解内部时求值器跳过对表达式的值推断例如第 442–443 行对无效二元表达式的处理以及第 681–682 行对布尔运算的处理因为字符串中的表达式并不存在于语义索引semantic index中无法也不需要对它做常规求值。同时字符串注解场景下ignore_runtime_errors为 true跳过了unsupported-operator的运行时检查——这从实现层面解释了为什么 4.1 节的示例没有任何诊断。五、诊断汇总与实验性提示需要特别留意的是/~本身属于ty 的实验性语法即使写法完全正确ty 也会给出experimental-syntax诊断见 experimental-syntax.md[environment] python-version 3.14class A: ... class B: ... def f(value: A B) - None: ... # error: [experimental-syntax] def g(value: ~A) - None: ... # error: [experimental-syntax]该诊断的规则说明指出实验性语法是 ty 特有、尚未进入 Python typing 规范的写法其他类型检查器可能拒绝接受也可能在将来被标准化或发生破坏性变更。源码侧分支通过self.context.report_lint(EXPERIMENTAL_SYNTAX, binary)上报type_expression.rs~分支同理同文件第 715–717 行诊断文案为 Negation type syntax is experimental。至此可以归纳出使用/~时的完整行为矩阵场景运行时行为ty 诊断ty 推断结果Python 3.14注解位置使用/~正常注解延迟求值experimental-syntax交集/否定类型值位置使用A B或~A必然TypeErrorunsupported-operator仍按交集/否定解释Python 3.13普通注解函数定义时抛TypeErrorunsupported-operator仍按交集/否定解释Python 3.13 from __future__ import annotations正常无运行时诊断交集/否定类型Python 3.13 手动字符串注解A B/~A正常无运行时诊断交集/否定类型六、实战要点小结优先在 Python 3.14 使用/~在注解位置直接可用语义与typing_extensions.Intersection等价但更简洁。记住优先级优先于|A B | C即(A B) | C左结合可链式书写A B C。不要在值位置使用A B、~A作为普通表达式会触发unsupported-operator诊断因为运行时必然抛TypeError。3.13 兼容方案加from __future__ import annotations或将注解写成字符串A B、~A即可既避免运行时错误、又获得与 3.14 一致的推断。预期实验性诊断即使一切合法也会看到experimental-syntax这是 ty 对未标准化语法的善意提醒可在确认团队及工具链支持后统一处理。用reveal_type验证像 mdtest 文档那样在函数体内使用reveal_type查看推断结果可以快速确认交集/否定的组合与优先级是否符合预期。若想深入验证本文所述行为可以直接运行该仓库的 mdtest 测试体系intersection_types.md位于 crates/ty_python_semantic/resources/mdtest/annotations/intersection_types.md与其相邻的 union.md、never.md 等用例共同构成了 ty 类型注解语法typing syntax的回归测试矩阵。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考