news 2026/9/10 8:20:11

Ruff ty 类型检查器整数比较类型推断:从 Literal 常量折叠到 unsupported-operator 诊断

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruff ty 类型检查器整数比较类型推断:从 Literal 常量折叠到 unsupported-operator 诊断

Ruff ty 类型检查器整数比较类型推断:从 Literal 常量折叠到 unsupported-operator 诊断

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

本篇技术指南基于 Ruff 仓库中crates/ty_python_semantic下的 mdtest 测试套件,深入剖析实验性静态类型检查器 ty 对Python 整数比较表达式==!=<<=>>=isis not,含链式比较)的推断规则。读者将掌握:整数字面量比较如何被折叠为Literal[True]/Literal[False]、整数实例比较为何退化为bool、跨类型比较(如1 == ""1 <= "")如何触发unsupported-operator诊断,以及这些行为背后的 Rust 源码实现位置。

测试文件与运行背景

本文的绝对主体是 integers.md,它隶属于 Ruff 类型检查器 ty 的 mdtest 测试套件(位于 resources/mdtest/comparison/)。该目录下还有 identity.md、unsupported.md、instances/rich_comparison.md 等姊妹测试,共同刻画比较表达式的推断语义。

mdtest 测试的语法特征非常直观:普通代码块中通过reveal_type(...)声明期望的类型,注释# revealed: <Type>即断言结果;# error: [unsupported-operator] "..."则断言某表达式必须产生指定错误码与消息。这些断言由 mdtest.py 驱动的测试框架执行。整个 comparison 目录可看作一份"可执行的类型语义规范"——本文讨论的每一条规则,都有对应可验证的测试用例背书。

整数字面量比较:常量折叠到Literal[bool]

integers.md 的第一组用例聚焦整数字面量之间的比较,此时类型检查器可以完全在编译期得出结论:

reveal_type(1 == 1 == True) # revealed: Literal[True] reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False] reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True] reveal_type(1 < 1) # revealed: Literal[False] reveal_type(1 > 1) # revealed: Literal[False]

观察这些结果可以归纳出三条核心规则:

  1. 同类型字面量比较被完全折叠:两个同值字面量比较出Literal[True],异值则出Literal[False]1 < 11 > 1这类"明显为假"的比较同样被折叠为Literal[False],说明检查器并没有偷懒退化为bool,而是做了精确求值。
  2. 链式比较按元素逐步求值1 == 1 == True拆分为1 == 1(真)与1 == True(真)两部分,因而整体为Literal[True]False < True <= 2 < 3 != 6的每一环都成立,整体仍为Literal[True]。这说明链式比较并非整体运算,而是a op1 b and b op2 c ...的逐段合取(Python 语言语义正是如此,但类型层面需要逐环求值才能给出常量结果)。
  3. boolint在比较层面互通True/False作为int的子类参与数值比较,False < True1 == True都能正常求值。

链式比较的布尔转换陷阱

ty 对链式比较有一个隐蔽但重要的行为:对前导元素(非最后一个元素)的比较结果隐式调用bool()。如果某个比较方法返回了不可布尔化的类型,就会触发unsupported-bool-conversion诊断。这在 instances/rich_comparison.md 中有专门用例:

class NotBoolable: __bool__: int = 3 class Comparable: def __lt__(self, item) -> NotBoolable: return NotBoolable() def __gt__(self, item) -> NotBoolable: return NotBoolable() 10 < Comparable() < 20

对应快照诊断为error[unsupported-bool-conversion]: Boolean conversion is not supported for typeNotBoolable``,并附带info:boolonNotBoolablemust be callable。值得注意的是Comparable() < Comparable()单独出现时是合法的(它是链尾,无需布尔转换),只有作为链中前导元素时才会报错——这与 Python 运行时"链式比较等价于a op1 b and b op2 c,中间结果必须可被and判定真假"的语义严格一致。

身份比较:isis not的独特推断

integers.md 中整数身份比较的结果非常反直觉,值得单独拆解:

reveal_type(1 is 1) # revealed: bool reveal_type(1 is not 1) # revealed: bool reveal_type(1 is 2) # revealed: Literal[False] reveal_type(1 is not 7) # revealed: Literal[True]
  • 1 is 1竟然无法确定:因为小整数虽常驻内存(CPython 对-5..256做了缓存),但类型系统不能假设两次出现的字面量1一定共享内存地址,它们"可能共享也可能不共享",因此结果是bool。同目录的 identity.md 给出了这条规则的完整表述:"two occurrences of the same literal1do not necessarily share the same memory address, as1is not a singleton (but they alsomight!)"。
  • 1 is 2可以确定为Literal[False]:两个不同的整数字面量必然不共享内存地址,所以身份比较恒为假。
  • 1 is not 7确定为Literal[True]:是上一条的对偶。

对照 identity.md 可以看到,False is False... is ...NotImplemented is NotImplemented都能折叠为Literal[True],因为这些是真正的单例对象;而普通整数没有单例保证,只能在"两个不同字面量"时断言恒假。

身份比较的源码实现

上述规则在 Rust 端的实现位于 comparisons.rs。其中Type::identity_comparison_truthiness方法(约第 126–177 行)是核心判定逻辑,返回Truthiness::{AlwaysTrue, AlwaysFalse, Ambiguous}三态:

  • 先用identity_comparison_type对两侧类型做"身份比较上转型":把NewType还原为具体基类(因为NewType构造器原样返回参数,两个标签不同的视图可能指向同一对象)、把带替换签名的函数字面量还原为原始函数对象、把 TypeVar 展开到上界/约束,从而聚焦"运行时可能是什么对象";
  • 然后若两侧类型互不相交is_disjoint_from),直接判定AlwaysFalse
  • 否则若两侧都是单例/含单例的交集,且一方是另一方的子类型,才判定AlwaysTrue
  • 其余情况为Ambiguous,即bool

这正是1 is 1 → bool(非单例、结果不确定)、1 is 2 → Literal[False](不同字面量在类型上不相交)两个测试断言的直接来源。

跨类型比较与unsupported-operator诊断

整数与字符串、布尔与字符串等异构类型比较是整数文档中最具诊断价值的部分:

reveal_type(1 == "") # revealed: Literal[False] reveal_type(1 != "") # revealed: Literal[True] # error: [unsupported-operator] "Operator `<=` is not supported between objects of type `Literal[1]` and `Literal[""]`" reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True] reveal_type(-1 < 0) # revealed: Literal[True]

这里展示了两类不同的行为:

  1. ==/!=对任意不匹配类型都"合法"intstr的相等比较在运行时总是返回False/True(因为 Python 对不兼容类型直接判定不等),因此类型层面可以精确折叠为Literal[False]/Literal[True],不产生任何诊断。这与object基类__eq__的默认行为一致。
  2. 有序比较(</<=/>/>=)要求类型兼容intstr之间不存在排序关系,因此1 <= ""触发error: [unsupported-operator]诊断,报错消息中会以Literal[1]Literal[""]的形式给出两侧精确类型。

值得注意1 <= "" and 0 < 1的推断结果:由于and左侧表达式已经出错(结果类型为Unknown,并带上& ~AlwaysTruthy的约束表示"它不能为真,因此短路不会走向右侧分支"之外的情形),整体被推断为(Unknown & ~AlwaysTruthy) | Literal[True]——即"要么左侧短路失败(Unknown 且不总是为真),要么右侧0 < 1恒真"。这展示了 ty 在错误表达式下仍然保持的精确约束传播。

unsupported-operator诊断在 builder.rs 与 diagnostic.rs 中定义与签发;unsupported.md 则系统性地覆盖了各类不支持操作符的报错快照,例如object() < 5<objectLiteral[5]间不支持)、(1, 2) < (1, "hello")(元组逐元素比较时定位到 index 2 处的Literal[2]Literal["hello"]不支持),可见诊断信息会尽力指出具体哪一环节、哪一对类型出了问题。

整数实例比较:退化为bool

当比较对象不再是字面量,而是整数类型的实例(变量、形参)时,ty 无法在编译期确定具体值,结果退化为bool

# TODO: implement lookup of `__eq__` on typeshed `int` stub. def _(a: int, b: int): reveal_type(1 == a) # revealed: bool reveal_type(9 < a) # revealed: bool reveal_type(a < b) # revealed: bool

文档中的TODO注释透露了一个重要实现现状:ty 目前尚未在 typeshed 的intstub 中查表__eq__等富比较双下划线方法,因此int实例(以及字面量与实例混合)的比较直接给出宽松的bool,而不是尝试调用 stub 里的签名。

这条行为与 instances/rich_comparison.md 所定义的一般规则形成对照:对于自定义类,==/!=/</<=/>/>=会分别查__eq____ne____lt____le____gt____ge__并返回其声明返回类型;当左侧方法缺失或参数类型不匹配时,会回退到右侧的反射方法(如100 > t回退到t.__lt__(100));两侧都匹配不上时==/!=回退为身份比较(得到bool)。int实例之所以一律得到bool,正是因为 stub 查找尚未落地,走的是最保守的默认路径。

归纳:整数比较推断决策表

将 integers.md 及其姊妹测试的规则汇总如下,便于作为速查表引用:

表达式形态推断结果依据与备注
同值整数字面量==/!=/</<=/>/>=Literal[True]常量折叠,见本文第一节用例
异值整数字面量比较Literal[False]同上
整数字面量链式比较(逐环均成立)Literal[True]每环单独求值后再合取
整数字面量链式比较(某环失败)Literal[False]同上
1 is 1/1 is not 1boolint非单例,见 identity.md
两个不同整数字面量isLiteral[False]运行时必不共享地址
1 is not 7Literal[True]上一条的对偶
intstr==/!=Literal[False]/Literal[True]不兼容类型相等比较恒为假/真
intstr做有序比较error: [unsupported-operator],结果为Unknown见 unsupported.md
int实例与字面量、int实例之间比较boolstub__eq__查找尚未实现,见文档 TODO 注释
负数与正数比较(-1 < 0Literal[True]支持符号处理的常量折叠

延伸阅读

  • 整数身份比较的完整语义与单例规则:identity.md
  • 富比较双下划线方法(含反射比较、子类优先级、字面量参数)的全量用例:instances/rich_comparison.md
  • unsupported-operator诊断的各种快照(含元组逐元素定位、in操作符):unsupported.md
  • 身份比较三态判定核心实现:comparisons.rs
  • unsupported-operator诊断定义:diagnostic.rs 与签发位置 builder.rs

综上,integers.md 虽然篇幅不大,却是 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/10 8:18:08

基于Matlab和粒子群的微电网V2G经济调度模型

我第一次跑通这个风光火电加电动汽车的微电网经济调度模型时&#xff0c;最大的感受不是“终于出结果了”&#xff0c;而是——为什么加了V2G之后&#xff0c;总成本反而更高了&#xff1f;后来一步步拆开看才发现&#xff0c;电池退化成本没有写进目标函数。这是这个领域最容易…

作者头像 李华
网站建设 2026/9/10 8:16:03

PEM电解槽COMSOL三维两相流模拟:多孔介质参数与实操要点

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

作者头像 李华
网站建设 2026/9/10 8:14:38

STM32并口LCD驱动原理与ILI9341适配实战

简介&#xff1a;本资源是正点原子推出的ILI9325/ILI9341 TFT LCD并口驱动工程&#xff0c;面向嵌入式初学者与STM32开发工程师&#xff0c;解决TFT液晶屏在裸机环境下基于并行接口的稳定驱动与显示适配问题。工程基于STM32F10x平台&#xff0c;完整包含LCD底层驱动&#xff08…

作者头像 李华
网站建设 2026/9/10 8:14:24

ByteTrack工业级部署实战:边缘芯片适配与参数调优

1. 这不是“又一个跟踪算法演示”&#xff0c;而是工业级多目标跟踪落地的实操切口最近在畅联云平台的开发者后台翻日志时&#xff0c;发现“ByteTrack”这个关键词的调用量三个月涨了4.7倍&#xff0c;其中83%的请求来自中小安防集成商和智能仓储系统厂商。很多人搜到的是论文…

作者头像 李华
网站建设 2026/9/10 8:10:27

直流电机H∞控制实战:从状态建模到鲁棒控制器设计

简介&#xff1a;本资源是一份面向控制工程领域研究生、科研人员及工程师的H∞鲁棒控制实战资料&#xff0c;聚焦直流电机在参数不确定性与外部扰动下的高性能闭环控制问题&#xff0c;系统覆盖状态空间建模、广义被控对象构建、权函数设计、H∞控制器综合与MATLAB仿真验证全流…

作者头像 李华
网站建设 2026/9/10 8:09:57

从生成内容到理解世界,AI 跨越新线,3D 或成其走进现实的关键

【导语&#xff1a;近年来AI不断迭代&#xff0c;但大多围绕“把内容做得更逼真”。本月体验Astra、Atlas、Cosmos后&#xff0c;作者认为AI正从“生成内容”迈向“理解世界、动手操作”&#xff0c;三维世界正被重写为AI接口&#xff0c;将带来新一轮价值转移。】Astra&#x…

作者头像 李华