news 2026/9/11 18:33:38

ruff 仓库 ty 类型检查器对 Walrus 运算符(赋值表达式)的类型推断详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ruff 仓库 ty 类型检查器对 Walrus 运算符(赋值表达式)的类型推断详解

ruff 仓库 ty 类型检查器对 Walrus 运算符(赋值表达式)的类型推断详解

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

本文以crates/ty_python_semantic/resources/mdtest/assignment/walrus.md为骨架,结合crates/ty_python_semanticcrate 中类型推断的实现源码,系统讲解 ty(ruff 仓库中正在开发、以 Rust 编写的 Python 类型检查器)如何对 PEP 572 引入的 Walrus 运算符(赋值表达式:=)进行类型推断,包括基础求值、自增模式((x := x + 1))的绑定更新,以及其在条件表达式、增强赋值、多目标赋值等场景下的行为,并给出在本地运行这些 Markdown 测试的具体方法。

一、Walrus 运算符与测试文档定位

Python 3.8(PEP 572)引入了赋值表达式,即 Walrus 运算符:=。它在表达式中完成赋值的同时返回被赋的值,因此x := 1既将1绑定到名字x,又让整个表达式求值得到1。由于形态类似海象(walrus)的牙齿,故得名。

在 ruff 仓库中,这一语言特性由类型检查器 ty 支持,其语义验证以"Markdown 即测试"(mdtest)的形式固化在crates/ty_python_semantic/resources/mdtest/assignment/walrus.md中。该文件虽然只有两个测试用例,却精确刻画了 Walrus 运算符类型推断的两条核心语义:

  1. 赋值表达式整体参与外层表达式运算x = (y := 1) + 1);
  2. Walrus 目标引用自身旧值进行更新(x := x + 1)),即"自增/自更新"模式。

根据 crates/ty_python_semantic/resources/README.md 的说明,mdtest/子目录下的所有 Markdown 文件都是"类型推断与类型检查"的测试套件,由tests/mdtest.rs集成测试执行;具体的测试书写规范见 crates/ty_test/README.md。

二、测试用例逐行解读

2.1 基础用例:表达式求值与字面量精确化

x = (y := 1) + 1 reveal_type(x) # revealed: Literal[2] reveal_type(y) # revealed: Literal[1]

这是整个测试文件中标注为## Basic的第一个用例,验证两条行为:

  • (y := 1)作为一个表达式,其值是1,因此(y := 1) + 1整体求值为2x被推断为Literal[2]
  • 赋值表达式内部完成了对名字y的绑定,y的类型是Literal[1]
  • 两侧的reveal_type断言使用了 mdtest 的# revealed:断言语法,要求与 ty 实际揭示的类型逐字符精确匹配(详见 crates/ty_test/README.md 的 "Assertions" 一节)。

其中Literal[2]Literal[1]这类"字面量类型"是 ty 精确推断能力的体现:它不仅能说出xint,还能说出"这个int的字面量值恒为2"。这一能力的底层数据结构是crates/ty_python_semantic/src/types/literal.rs中定义的LiteralValueType<'db>(见该文件第 15 行)及其IntLiteralTypeStringLiteralTypeBytesLiteralTypeEnumLiteralType等内部类型,整数字面量类型通过From<i64> for LiteralValueTypeKind(第 268 行)等转换直接承载具体值。

2.2 自增用例:Walrus 自引用旧值

x = 0 (x := x + 1) reveal_type(x) # revealed: Literal[1]

标注为## Walrus self-addition的第二个用例,验证的是"Walrus 更新已有绑定"的语义:

  • 首先x = 0x绑定为Literal[0]
  • 随后(x := x + 1)在执行赋值表达式时,右侧的x + 1读取的是x的旧值0,运算得1,再回写绑定x
  • 因此最终x被更新为Literal[1]

这里的关键点是:Walrus 运算符"读旧值、算新值、写回同一名字"的三步顺序。与 Python 运行时的求值顺序一致,ty 的类型推断也必须保证在推断x + 1时使用的是更新前的绑定类型,而不是回写后的类型——这正是实现中"先推断右侧值、再插入绑定"顺序的由来(见下一节)。

三、源码级实现:Walrus 类型推断的核心调用链

两个测试用例背后,是crates/ty_python_semantic/src/types/infer/builder.rs中两处紧密配合的实现。

3.1 作为表达式:infer_named_expression

当 ty 的类型推断器(inference builder)遇到 AST 中的ast::Expr::Named(即 Walrus 赋值表达式)时,会分派到infer_named_expression(builder.rs):

fn infer_named_expression(&mut self, named: &ast::ExprNamed) -> Type<'db> { // See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements if named.target.is_name_expr() && !self.in_string_annotation() { let definition = self.index.expect_single_definition(named); let result = infer_definition_types(self.db(), definition); self.extend_definition(definition, result); result.binding_type(definition) } else { // String annotations have no indexed definitions, and syntactically invalid targets // cannot define a name. Both sides still need inference to preserve their diagnostics. self.infer_expression(&named.target, TypeContext::default()); self.infer_expression(&named.value, TypeContext::default()); Type::unknown() } }

从源码可以读出三条重要信息:

  1. 主路径按"定义"处理:当目标是普通名字(is_name_expr())且不在字符串注解中时,ty 通过self.index.expect_single_definition(named)找到该 Walrus 表达式对应的唯一"定义"(Definition),再调用infer_definition_types对定义做类型推断,最终以result.binding_type(definition)返回绑定类型作为整个表达式的结果类型。这就解释了(y := 1) + 1中表达式的结果为何是Literal[1]——它返回的正是y这条绑定所持有的类型。

  2. 兜底路径仍要保留诊断:当目标是字符串注解(无索引定义)或语法上无法定义名字的非法目标时,两侧(target 与 value)依然会被infer_expression各推断一遍,以保证相关诊断不丢失,但表达式整体返回Type::unknown()

  3. 注释直接引用 PEP 572:源码中保留了指向 PEP 572 "Differences between assignment expressions and assignment statements" 一节的链接,说明该实现刻意对照了规范中"赋值表达式与赋值语句的差异"来设计。

3.2 作为定义:infer_named_expression_definition

Walrus 对名字的绑定本身是一条Definition。推断器在infer_definition的分发中,遇到DefinitionKind::NamedExpression(named_expression)时进入infer_named_expression_definition(builder.rs)。其实现位于 builder.rs:

fn infer_named_expression_definition( &mut self, named: &'ast ast::ExprNamed, definition: Definition<'db>, ) -> Type<'db> { let ast::ExprNamed { range: _, node_index: _, target, value, } = named; let add = self.add_binding(named.target.as_ref().into(), definition); let ty = self.infer_expression(value, add.type_context()); self.store_expression_type(target, ty); add.insert(self, ty) }

这段实现精确对应了 2.2 节描述的"读旧值、算新值、写回"语义:

  • self.add_binding(...)先为target(如x)建立绑定;
  • self.infer_expression(value, ...)随后才对右侧value(如x + 1)进行推断——此刻对x的读取命中的仍是旧绑定(Literal[0]),因此x + 1被推为Literal[1]
  • 最后store_expression_type记录目标的表达式类型,add.insert(self, ty)把新类型Literal[1]写回绑定。

这一"先登记绑定、后推断右侧、最后插入结果"的顺序,正是(x := x + 1)能推出Literal[1]的机制保证;若顺序颠倒为"先推断右侧再登记绑定",右侧对x的读取就可能拿到未定义或错误的类型。

3.3 从表达式到定义的联通

两条路径通过DefinitionDefinitionKind::NamedExpression相互衔接:外层表达式求值(3.1)与内层名字绑定(3.2)共享同一节点,从而保证"表达式结果类型"与"绑定类型"始终一致——这正是xy在 2.1 用例中被同时精确推断的前提。

四、Walrus 在周边场景中的协同语义

assignment/目录下的其他测试文件与walrus.md同属一个测试套件家族,从侧面印证了 Walrus 语义在 ty 中的整体地位:

4.1 与增强赋值(augmented assignment)结合

crates/ty_python_semantic/resources/mdtest/assignment/augmented.md 的## Walrus target一节展示了(ys := [1])[0] += 1的写法:Walrus 的返回值(一个列表)被立即作为下标赋值的左值。这说明 Walrus 表达式的结果可以是任何可索引对象,其类型(此处为list[int | str])随后参与元素类型与操作符的检查。

4.2 与多目标赋值(multi-target assignment)结合

crates/ty_python_semantic/resources/mdtest/assignment/multi_target.md 中"共享值里的赋值表达式"一节验证了更精细的规则:first = second = (named := lambda: 0)中,named的绑定只发生一次,且所有目标(firstsecondnamed)都获得同一个可调用类型() -> Literal[0];而"赋值表达式中的上下文推断"一节则表明,当named带有声明类型Callable[[int], int]时,该声明类型会反过来为共享的 lambda 提供参数上下文。

4.3 在条件表达式中的特殊处理

ty 在冗余条件(redundant condition)诊断中,对包含 Walrus 的表达式有专门分类。crates/ty_python_semantic/src/types/infer/builder/redundant_conditions/mod.rs中:

  • 定义了ConditionKind::ContainsWalrus(第 115 行),并说明"Walrus 表达式是例外:它们总是有副作用"(第 105 行注释);
  • 通过any_over_expr(expression, ast::Expr::is_named_expr)检测条件中是否含有:=(第 563-565 行),命中即归类为ContainsWalrus
  • crates/ty_python_semantic/src/types/infer/builder/redundant_conditions/exemptions.rs中,redundant-condition-strict规则对含 Walrus 的条件(如assert (value := "foo"))予以豁免(第 70 行)。

这从诊断角度说明:ty 认识到 Walrus 即使出现在看似"冗余"的条件下也必然产生副作用(赋值),因此不应以静态真值判断将其判为冗余——这与 PEP 572 的语义是一致的。

五、如何本地运行这些 Walrus 测试

这些 Markdown 测试是可直接运行的回归测试,运行方式有两条(详见 crates/ty_test/README.md 的 "Running the tests" 一节与 crates/ty_python_semantic/mdtest.py):

方式一:通过 cargo 过滤运行

cargo test -p ty_python_semantic -- mdtest

该命令会运行ty_python_semanticcrate 下所有以mdtest为名的测试(即tests/mdtest.rs驱动的全部 Markdown 套件)。也可进一步缩小范围,例如仅针对 walrus 相关测试:

cargo test -p ty_python_semantic --test mdtest -- walrus

方式二:使用带监听模式的 Python runner

uv run crates/ty_python_semantic/mdtest.py

mdtest.py基于richwatchfiles(见其文件头部的依赖声明),支持监听模式:当 Markdown 测试文件变化时自动重跑对应测试,当 Rust 源码变化时自动重新编译,适合在开发类型推断逻辑时反复迭代。

测试如何工作:框架会把每个 Markdown 文件视为一个测试套件,其中被三个反引号围栏包裹的py代码块会被写入内存文件系统(默认路径/src/mdtest_snippet.py)并进行类型检查,随后用代码内的断言注释(# revealed:# error:)与产生的诊断逐一比对,全部匹配才算通过。例如walrus.mdreveal_type(x) # revealed: Literal[2]断言"第 3 行的x被揭示为Literal[2]",若推断结果变为int或其他类型,测试即失败。每个测试在相互独立的 Salsa 数据库中从头执行,互不共享状态。

六、小结

crates/ty_python_semantic/resources/mdtest/assignment/walrus.md以两个短小精悍的用例,划定了 ty 对 Walrus 运算符类型推断的核心语义边界:

用例核心断言对应实现
Basic表达式整体求值 + 绑定同时生效,Literal[2]/Literal[1]infer_named_expression(builder.rs)
Walrus self-addition读旧值、算新值、回写绑定,Literal[0] → Literal[1]infer_named_expression_definition(builder.rs)

结合augmented.mdmulti_target.md中关于 Walrus 目标的增强赋值、共享值绑定、上下文推断,以及redundant_conditions中对 Walrus 副作用的豁免处理,可以完整看到 ty 对赋值表达式的支撑面:既能在表达式层级精确求值,也能在定义层级正确处理自引用更新,还能在诊断层面尊重其必然副作用。对希望为 ty 贡献类型推断逻辑、或想深入理解"Rust 实现的 Python 类型检查器如何处理表达式级副作用"的开发者来说,这个文件及其背后不足百行的推断实现,是一个小而完整的入口。

【免费下载链接】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/11 18:33:03

10分钟快速上手 GhostTrack:终端位置追踪 OSINT 工具保姆级指南

10分钟快速上手 GhostTrack&#xff1a;终端位置追踪 OSINT 工具保姆级指南 【免费下载链接】GhostTrack Useful tool to track location or mobile number 项目地址: https://gitcode.com/GitHub_Trending/gh/GhostTrack GhostTrack 是一个完全在终端里运行的轻量 OSIN…

作者头像 李华
网站建设 2026/9/11 18:30:39

系统分析实战:从需求到方案的关键技术与方法

1. 系统分析概述&#xff1a;从需求混沌到方案落地的关键跃迁系统分析是软件工程中那个"把模糊需求变成清晰方案"的关键环节。作为从业15年的老分析师&#xff0c;我常把这个过程比作医生问诊——用户描述症状&#xff08;业务需求&#xff09;&#xff0c;我们通过专…

作者头像 李华
网站建设 2026/9/11 18:30:23

### Python集群导向计算:构建高效分布式数据处理架构

随着数据规模的爆炸式增长&#xff0c;单机计算在处理海量数据、复杂模型训练及高并发任务时逐渐显露出性能瓶颈。Python集群导向&#xff08;Cluster-oriented&#xff09;计算通过将任务分发至多个计算节点并行执行&#xff0c;打破了单机的物理限制&#xff0c;成为现代数据…

作者头像 李华
网站建设 2026/9/11 18:29:37

【读书笔记】《屏幕前的童年》

《屏幕前的童年》内容整理 书名:《屏幕前的童年》 副标题:科技产品、商业主义与我们的孩子 作者:苏珊林恩(Susan Linn),美国心理学家,哈佛医学院精神病学讲师,长期研究儿童、商业与媒体的关系一、为什么孩子容易沉迷电子产品? 核心观点:不是孩子自制力差,而是背后有一整套成熟…

作者头像 李华