news 2026/9/12 6:35:19

Ruff/ty 一元运算符类型推断深入解析:UAdd、USub 与 Invert 的实现原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruff/ty 一元运算符类型推断深入解析:UAdd、USub 与 Invert 的实现原理

Ruff/ty 一元运算符类型推断深入解析:UAdd、USub 与 Invert 的实现原理

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

本文基于 ruff 仓库中ty类型检查器的官方 mdtest 用例 invert_add_usub.md,系统讲解一元正号+、一元负号-与按位取反~三类一元运算符在类型推断中的求值规则。你将掌握:reveal_type在 unary 场景下的预期输出、unsupported-operator诊断的触发条件与消息格式、__pos__/__neg__/__invert__三个 dunder 方法的调用策略,以及带边界(bound)与受约束(constrained)的TypeVar在一元运算中的特殊处理逻辑。全文以仓库源码 builder.rs 为底层依据,可直接对照验证。

一、文档定位:mdtest 是什么

ty是 ruff 仓库中的 Python 类型检查器(type checker),其核心类型推断逻辑位于crates/ty_python_semantic目录。该目录下的resources/mdtest/存放了大量以 Markdown 编写的、可执行的类型检查测试用例(mdtest,即 markdown test)。

每个 mdtest 文件由"普通 Markdown 说明 + 带reveal_typeerror:注释的 Python 代码块 + 可选的snapshot快照块"组成。类型检查器会执行这些用例,将reveal_type(x)展开为实际推断出的类型,并与注释中的# revealed:期望值比对;# error: [rule-code]则用于断言诊断是否在指定代码行触发。

invert_add_usub.md 正是这个测试体系中对+-~三个一元运算符的专项测试。与它同目录的还有 not.md(一元not)、custom.md(自定义一元运算与继承行为)、integers.md(整数与字面量的一元运算),共同构成完整的 unary 测试矩阵。本文聚焦于invert_add_usub.md本身,结合源码验证其行为。

二、三类一元运算符的语义与 dunder 方法映射

Python 语言规范中,+x-x~x分别调用操作数的__pos____neg____invert__方法。ty 类型检查器在推断一元表达式时同样遵循这一约定,其 dunder 方法映射在 builder.rs 中明确定义:

一元运算符Python 语法对应的 dunder 方法典型语义
+UAdd(Unary Add)__pos__一元正号
-USub(Unary Subtract)__neg__一元负号
~Invert__invert__按位取反

在 AST 层面,这三者属于ast::UnaryOp枚举的UAddUSubInvert变体,与布尔取反Not并列。类型推断入口为 infer_unary_expression:先对操作数递归求值得到operand_type,再调用 infer_unary_expression_type 按"运算符 + 操作数类型"的组合分发处理。

需要特别指出:Not不在此文档讨论范围内(它走独立的真值推断路径,见 not.md)。本文只覆盖+-~

三、实例(Instance)场景:dunder 方法的调用与错误报告

3.1 定义了 dunder 方法的类

文档第一个测试块展示了最核心的实例场景:

from typing import Literal class Number: def __init__(self, value: int): self.value = 1 def __pos__(self) -> int: return +self.value def __neg__(self) -> int: return -self.value def __invert__(self) -> Literal[True]: return True a = Number(0) reveal_type(+a) # revealed: int reveal_type(-a) # revealed: int reveal_type(~a) # revealed: Literal[True]

推断结果清晰呈现了三条规则:

  1. +a的结果类型是Number.__pos__的返回类型int
  2. -a的结果类型是Number.__neg__的返回类型int
  3. ~a的结果类型是Number.__invert__的返回类型Literal[True]——注意这里连字面量类型(Literal type)都被完整保留,说明一元运算符的结果类型完全由 dunder 方法的返回类型精确决定。

从源码看,这一行为对应 fallback_unary_expression_type 中的try_call_dunder调用:以CallArguments::none()(dunder 方法隐式接收self,无需额外参数)调用对应方法,成功则返回其返回类型(outcome.return_type(db, env)),并顺带执行废弃绑定检查(check_deprecated_bindings)。

3.2 未定义 dunder 方法的类:unsupported-operator

第二个测试块定义了完全不实现任何一元 dunder 的类NoDunder

class NoDunder: ... b = NoDunder() +b # error: [unsupported-operator] "Unary operator `+` is not supported for object of type `NoDunder`" -b # error: [unsupported-operator] "Unary operator `-` is not supported for object of type `NoDunder`" ~b # error: [unsupported-operator] "Unary operator `~` is not supported for object of type `NoDunder`"

try_call_dunder返回错误时,推断器调用 report_unsupported_unary_operator 生成诊断。该诊断的规则为unsupported-operator,在 diagnostic.rs 中注册:

declare_lint! { #[doc = include_str!("../../resources/lint_docs/unsupported-operator.md")] pub(crate) static UNSUPPORTED_OPERATOR = { summary: "detects binary, unary, or comparison expressions where the operands don't support the operator", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, } }

诊断消息模板为"Unary operator \{op}` is not supported for object of type `{operand_type}`",其中op+/-/~的源码文本,operand_type是操作数的显示类型。这与 mdtest 中断言的错误文本完全一致。规则级别为Error(稳定状态自0.0.1-alpha.1),与unresolved-referenceinvalid-attribute-access` 等检查同属稳定错误规则。

值得一提的是错误报告还具备细化能力:当 dunder 调用失败源于"方法可能未绑定"(CallDunderError::PossiblyUnbound)时,会额外追加\{ty}` does not implement `{unary_dunder_method}`` 信息行,帮助用户定位到具体哪个类型成员缺少对应方法。

3.3 相关补充:继承与类对象

虽然不在invert_add_usub.md内,但同目录的 custom.md 提供了与本文档互补的关键行为,可帮助理解 dunder 的查找范围:

  • 继承生效:子类实例会继承父类的__pos__/__neg__/__invert__reveal_type(+Sub())同样得到父类方法的返回类型;
  • 类对象不支持:dunder 方法定义在类上,仅对其实例生效,对类本身不生效——reveal_type(+Yes)会触发unsupported-operator,因为要让类对象支持一元运算,方法必须定义在type上;
  • 失败时的回退类型:当 dunder 缺失导致报错时,表达式的结果类型回退为Unknown(见 builder.rs 的e.fallback_return_type分支)。

四、字面量类型的快速路径

在调用 dunder 方法之前,推断器对字面量类型(Type::LiteralValue)提供了"快速路径"(fast path),直接计算结果而不经过方法查找,具体规则位于 builder.rs:

运算符字面量类型结果实现说明
+int字面量保持原值的int字面量Type::int_literal(value.as_i64())
+bool字面量0/1int字面量i64::from(value)
-int字面量取负后的int字面量使用checked_neg();溢出(如-(-2^63))时回退为普通int实例
-bool字面量0/-1int字面量-i64::from(value)
~int字面量按位取反后的int字面量Type::int_literal(!value.as_i64())
~bool字面量0/-1int字面量Type::int_literal(!i64::from(value))

这里有两处值得注意的实现细节:

  1. 负号溢出保护-i64::MIN在数学上会溢出,源码使用checked_neg(),溢出时回退到KnownClass::Int的普通实例类型,避免产生错误的字面量结果;
  2. ~bool的废弃检查:typeshed 中bool.__invert__已被标记为废弃(deprecated),因此~作用于bool字面量时,推断器会先通过member_lookup_with_policy查找__invert__并执行废弃检查(check_deprecated),再返回计算结果。源码注释明确说明:之所以只为~bool做这项检查,是因为int.__neg__之类的 dunder 被废弃的可能性微乎其微,不值得为所有字面量快速路径都付出额外查找开销。

除上述匹配的字面量种类外,其余字面量(如字符串、浮点字面量等)会落入fallback_unary_expression_type()走正常的 dunder 调用路径。

五、TypeVar 的一元运算:边界与约束的三种情形

文档的第二大部分(自## TypeVar with bounds起)专门测试 TypeVar 场景,这也是本 mdtest 最具深度的部分。对应的源码分支在 builder.rs,源码注释明确将其标注为对受约束 TypeVar 的特殊处理,并指出未来迁移到新 solver 后会有更通用的方案。

5.1 边界为float的 TypeVar:委托给边界类型

from typing import TypeVar T = TypeVar("T", bound=float) def neg_float_bound(a: T) -> float: reveal_type(-a) # revealed: float return -a def pos_float_bound(a: T) -> float: reveal_type(+a) # revealed: float return +a

文档给出了关键说明:在类型注解中,float被视为int | float的联合类型,因此以float为边界的 TypeVar 应当支持一元+-

这一特殊约定在源码中有多处印证:

  • newtype.rs 注释:floatcomplex在类型位置上被特殊处理,分别指向int | floatint | float | complex
  • set_theoretic.rs 中Float成员被定义为int | float
  • 一元推断的分支Some(TypeVarBoundOrConstraints::UpperBound(bound))会递归调用infer_unary_expression_type(op, bound, unary),即将运算符直接作用于边界类型进行推断(builder.rs)。

于是-a+a的类型按int | float参与推断,最终结果为floatint | float在显示上折叠为float)。

5.2 边界为int的 TypeVar:支持全部数值一元运算

U = TypeVar("U", bound=int) def neg_int_bound(a: U) -> int: reveal_type(-a) # revealed: int return -a def invert_int_bound(a: U) -> int: reveal_type(~a) # revealed: int return ~a

int为边界的 TypeVar 与float边界走同一条UpperBound委托路径,区别在于边界类型是int而非int | floatint原生支持__neg____invert__,因此-a~a均推断为int。文档点明:只要边界类型支持对应运算符,带边界的 TypeVar 就支持该一元运算符

5.3 受约束的 TypeVar:逐约束检查与类型保持

V = TypeVar("V", int, float) def neg_constrained(a: V) -> V: reveal_type(-a) # revealed: V@neg_constrained return -a

受约束 TypeVar(TypeVar("V", int, float))走的是完全独立的分支TypeVarBoundOrConstraints::Constraints(constraints),源码逻辑在 builder.rs,包含两个关键设计:

  1. 对每个约束逐个调用 dunder:先对所有约束try_call_dunder并收集结果,这一步在报告错误前完成,目的是让废弃(deprecation)报告不依赖"是否有约束失败";
  2. map_constrained_typevar_constraints映射结果:将每个约束的一元运算结果类型映射回原 TypeVar。当所有约束都支持该运算符且返回类型一致时(如-int -> int-float -> float的显示类型同为float),映射成功,TypeVar 被保留,因此-a的推断结果为V@neg_constrained(函数内的 TypeVar 实例,带函数名后缀以区分不同作用域的 TypeVar);
  3. 部分失败时:只要有一个约束不支持该运算符,map_constrained_typevar_constraints返回None,此时触发unsupported-operator诊断,并以整个 TypeVar 的回退调用结果作为兜底类型。

六、操作数类型的快速跳过与分发总览

除上述主要路径外,infer_unary_expression_type 还处理了若干特殊操作数类型,形成完整的分发表:

操作数类型行为
DynamicAny相关)+/-/~直接返回操作数自身类型,不调用 dunder
DivergentNoReturn等)直接返回操作数类型
Never返回Never
TypeAlias解包别名后递归推断其值类型
TypeVar(上界/约束)按第五节所述三种情形处理
其余类型(函数、可调用、模块、类、联合、交集、TypedDictNewType实例等)统一走fallback_unary_expression_type(),即 dunder 调用路径,失败则报unsupported-operator

从这份分发表可以提炼出 ty 对一元运算的设计原则:能用字面量常量折叠就折叠,能用类型代数直接推导就推导,其余情况一律通过 dunder 方法调用收口,保证了推断结果的精确性与可预测性。

七、如何本地运行与验证这些用例

若要亲身体验这些类型推断行为,可使用仓库自带的测试基础设施:

  1. 运行整个 unary 测试目录:在仓库根目录执行 cargo 测试命令,让ty_python_semantic的测试框架扫描crates/ty_python_semantic/resources/mdtest/下的全部 mdtest 文件并逐一断言(包括invert_add_usub.md、not.md、custom.md);
  2. 修改与新增用例:mdtest 文件即"文档即测试"——在 Markdown 的 Python 代码块中添加reveal_type# error:注释即可扩展覆盖,快照(snapshot块)用于锁定错误输出的精确格式;
  3. 对照源码:类型推断逻辑集中在 crates/ty_python_semantic/src/types/infer/builder.rs,一元运算入口为 infer_unary_expression,错误规则定义见 crates/ty_python_semantic/src/types/diagnostic.rs。

八、小结

以 invert_add_usub.md 为骨架,结合源码可以总结出 ty 对+-~三类一元运算符的完整推断策略:

  • 实例对象:通过try_call_dunder调用__pos__/__neg__/__invert__,成功则返回 dunder 的返回类型(保留字面量精度),失败则触发Error级别的unsupported-operator诊断并回退为Unknown
  • 字面量:对int/bool字面量走常量折叠快速路径,包含溢出保护(checked_neg)与~bool废弃检查两个细节处理;
  • 带边界的 TypeVarfloat边界等价于int | float联合,int边界直接支持,均通过委托给上界类型推断,结果为边界类型;
  • 受约束的 TypeVar:逐个约束调用 dunder 并映射结果,全部约束支持且返回类型一致时保持 TypeVar 类型(如V@neg_constrained),任一约束失败则报错;
  • 其余类型:统一走 dunder 回退路径,构成兜底。

这套"mdtest 文档 + 源码实现"的组合,既是 ty 类型检查器的一元运算行为规范,也是理解 ruff 仓库如何用"可执行文档"驱动并验证类型推断正确性的最佳样例。

【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 6:34:48

OpenCV仍是多模态视觉开发基本功:从预处理到Agent落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 6:31:53

SpringMVC大文件分块上传优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 6:31:45

PaddlePaddle 源码工程审阅:从算子实现到内存管理的架构实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 6:30:37

Superpowers技能包:让AI编码代理遵循TDD与任务拆解高效工作

Superpowers 这个项目,名字起得相当直白——给 AI 编码代理“超能力”。如果你已经在用 Codex CLI、Claude Code 这类跑在终端里的 AI 编程工具,大概率会遇到同一个瓶颈:模型本身很聪明,但真让它独立完成一个有点复杂的任务时&…

作者头像 李华
网站建设 2026/9/12 6:27:49

ASP.NET技术体系解析与开发实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华