十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ruff 类型检查器(ty)的 if 表达式类型推断:短路语义、Never 分支与字面量联合类型

Ruff 类型检查器(ty)的 if 表达式类型推断:短路语义、Never 分支与字面量联合类型 Ruff 类型检查器ty的 if 表达式类型推断短路语义、Never 分支与字面量联合类型【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本文以仓库内 crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md 这份可执行测试文档为主线系统讲解 Ruff 内置类型检查器ty对 Python 条件表达式x if cond else y的类型推断规则包括分支类型如何合并为联合类型、短路求值如何锁定单一分支、与Never等价的操作数如何被识别以及条件真值转换失败时的诊断行为。读完本文你将掌握 ty 在分支级精确性上的核心设计并知道如何在本地运行这些 Markdown 测试来验证任意条件表达式if 表达式、if语句、match守卫、推导式过滤器的推断结果。一、背景一份 Markdown 文件就是一套可执行测试在 Ruff 的 ty 类型检查器crates/ty_*系列 crate中任何 Markdown 文件都可以是一套类型推断测试ty_test::run会把文件中以三反引号包裹的py/pyi/toml代码块写入内存文件系统运行一次类型检查再把产生的诊断与文件内的注释断言逐一比对。断言主要有两种形式reveal_type(x) # revealed: 类型调用reveal_type打印x的推断类型注释中的内容必须与打印结果逐字一致# error: [规则码] 消息片段断言该行会产生某个诊断可用方括号指定规则码、双引号限定消息内容。完整的框架说明见 crates/ty_test/README.md。也就是说本文要讲的 if_expression.md 不只是一篇文档而是一套可直接运行、可回归验证的类型推断规格说明。它与同目录下的 if_statement.md、match.md 以及 boolean/short_circuit.md 共同构成了条件语义的测试矩阵。二、源码入口infer_if_expression的分支合并与真值判定if 表达式AST 节点ast::ExprIf的推断入口位于 crates/ty_python_semantic/src/types/infer/builder.rs 的infer_if_expression。其核心流程可以概括为三步分别推断三个组成部分条件test、真分支body、假分支orelse。对于集合字面量列表/字典/集合还会让两个分支互为类型上下文collection_literal_peer_context使[1] if flag else []这类表达式能推断出list[int]而不是list[Unknown]对条件做真值判定先通过test_ty.try_bool(db, env)尝试把条件转换为bool再调用analyze_condition_expression对条件做短路分析得到三种结果之一——Truthiness::AlwaysTrue、Truthiness::AlwaysFalse或Truthiness::Ambiguous按真值结果选择分支结果match test_truthiness { Truthiness::AlwaysTrue body_ty, Truthiness::AlwaysFalse orelse_ty, Truthiness::Ambiguous UnionType::from_two_elements(db, env, body_ty, orelse_ty), }可以看到只要条件能被静态判定为恒真或恒假if 表达式就只会取其中一个分支的类型只有条件真值无法确定时结果才是两个分支类型的联合。这正是本文后续所有例子的理论基础。此外try_bool失败例如条件对象不可做布尔转换时会调用err.report_diagnostic上报诊断并用err.fallback_truthiness()决定分支走向——这正是最后一节unsupported-bool-conversion诊断的产生路径。三、基础形态简单、嵌套、None 与海象运算符3.1 简单 if 表达式结果即为分支联合def _(flag: bool): x 1 if flag else 2 reveal_type(x) # revealed: Literal[1, 2]flag类型为bool真值未知因此推断结果为两个分支字面量类型的联合Literal[1, 2]。注意这里展示的是字面量类型而非宽泛的int——ty 对常量表达式会保留字面量精度这对后续控制流分析非常关键。3.2 嵌套 if 表达式右结合展开def _(flag: bool, flag2: bool): x 1 if flag else 2 if flag2 else 3 reveal_type(x) # revealed: Literal[1, 2, 3]Python 的条件表达式是右结合的1 if flag else 2 if flag2 else 3等价于1 if flag else (2 if flag2 else 3)。三个字面量分支各自可达联合后为Literal[1, 2, 3]。3.3 None 分支联合出Nonedef _(flag: bool): x 1 if flag else None reveal_type(x) # revealed: Literal[1] | None分支类型可以是None此时联合类型写作Literal[1] | None等价于常见的可选值形态。3.4 海象运算符walrus分支赋值可达性影响变量类型def _(flag: bool): y 0 z 0 x (y : 1) if flag else (z : 2) reveal_type(x) # revealed: Literal[1, 2] reveal_type(y) # revealed: Literal[0, 1] reveal_type(z) # revealed: Literal[0, 2]这是最有信息量的例子之一。x是常规的分支联合Literal[1, 2]但y和z的类型揭示了 ty 对赋值可达性的建模若flag为真执行y : 1z保持初始值0若flag为假执行z : 2y保持初始值0。因此y的最终类型是初始值 ∪ 真分支赋值Literal[0, 1]z同理为Literal[0, 2]。这说明分支内的海象赋值只会把新类型并入变量的联合类型而不是覆盖其既有类型。四、静态已知的复合条件短路如何锁定单一分支这一节是 if 表达式推断中最微妙的部分核心观点是短路条件即使包含真值可变mutable truthiness的操作数仍可能选择单一分支但把条件值保存下来再测试就不再有同样的保证。4.1and/or的恒假 / 恒真def _(value: object): reveal_type(1 if value and False else 2) # revealed: Literal[2] reveal_type(1 if value or True else 2) # revealed: Literal[1] saved value and False reveal_type(1 if saved else 2) # revealed: Literal[1, 2]value and False无论value的真值如何and的结果要么是假值value当value为假时短路返回value要么是恒假的False因此整个条件恒假只走else分支 →Literal[2]。同理value or True恒真 →Literal[1]。关键在于saved value and False之后再测试saved结果却是Literal[1, 2]。原因在 boolean/short_circuit.md 的 Reachability of compound conditions 一节中有详细解释value可能是某个__bool__每次调用返回不同结果的可变真值对象。作为分支条件时短路直接落到控制流决策value.__bool__只被测试一次而保存到变量后if saved会再次测试其真值两次结果可能不同因此真值分支保持可达两个分支都需要保留 →Literal[1, 2]。4.2 比较链即使单个比较返回任意对象class Comparable: def __lt__(self, other: int) - object: return object() def _(value: Comparable): reveal_type(1 if value 1 0 else 2) # revealed: Literal[2] saved value 1 0 reveal_type(1 if saved else 2) # revealed: Literal[1, 2]value 1调用__lt__返回object()——真值恒真的任意对象于是比较链继续求值1 0而1 0静态恒假整个链恒假 →Literal[2]。但保存链的结果后saved的类型中包含了真值不可静态确定的成分再次测试可能得到不同真值 →Literal[1, 2]。这与 short_circuit.md 中 Chained comparison conditions 的结论完全一致作为条件时任意一环恒假即整链恒假保存后再测则真值分支保持可达。4.3 操作数收窄为Never的嵌套条件def _(other: object, value: bool): reveal_type(1 if other and (isinstance(value, str) and value) else 2) # revealed: Literal[2] reveal_type(1 if other or (not isinstance(value, str) or value) else 2) # revealed: Literal[1]value: bool不可能同时是str因此isinstance(value, str)恒假isinstance(value, str) and value的第二个操作数value被收窄为Never不可达。于是other and (…)无论other如何内层and只能产生假值结局外层and恒假 →Literal[2]other or (not isinstance(value, str) or value)not isinstance(value, str)恒真内层or恒真外层or恒真 →Literal[1]。即使外层操作数other的真值可变短路仍能保留剩余部分静态已知的结局——这正是短路语义可穿透嵌套布尔运算的体现。五、与Never等价的操作数调用与类型变量5.1 返回Never别名的调用[environment] python-version 3.12from typing import Never type Bottom Never def stop() - Bottom: raise RuntimeError def _(flag: bool): reveal_type(1 if flag and stop() else 2) # revealed: Literal[2] reveal_type(1 if flag or stop() else 2) # revealed: Literal[1]stop()的返回类型是Never的类型别名Bottom它永远无法产生结果函数只抛异常不返回。因此flag and stop()当flag为真时求值stop()永不返回真分支不可达 → 只保留Literal[2]flag or stop()当flag为假时求值stop()永不返回假分支不可达 → 只保留Literal[1]。注意文件开头的[environment] python-version 3.12TOML 配置块mdtest 支持按章节层级继承配置详见 mdtest_config.md这里指定 Python 3.12 是因为示例使用了 PEP 695 的type语句与类型参数语法。5.2 受Never约束的类型变量def _T: Never: reveal_type(1 if flag and value else 2) # revealed: Literal[2]类型变量T的上界是Never意味着value不可能有实际取值。flag and value的真分支需要value为真而value无法产生任何结果 → 真分支不可达只剩Literal[2]。这与调用返回Never的场景共享同一条操作数等价于Never的判定逻辑。六、__bool__实现错误unsupported-bool-conversion诊断条件表达式必然要求把条件转换为布尔值。如果对象把__bool__声明成了普通属性而不是可调用方法布尔转换就无法进行class NotBoolable: __bool__: int 3 # error: [unsupported-bool-conversion] Boolean conversion is not supported for type NotBoolable 3 if NotBoolable() else 4__bool__被声明为int类型的属性ty 在把NotBoolable()转换为条件真值时发现没有可用的__bool__方法于是产生规则码为unsupported-bool-conversion的诊断消息为 Boolean conversion is not supported for typeNotBoolable。注释断言# error: [规则码] 消息片段同时校验了规则码与消息文本。对应到源码这一诊断正是infer_if_expression中test_ty.try_bool(db, env)返回Err后由err.report_diagnostic(...)上报的随后err.fallback_truthiness()给出条件真值的兜底结论保证诊断之后推断仍能继续进行。七、在本地运行与验证由于这套 Markdown 文档就是可执行测试你可以在仓库内直接运行来验证上述所有推断结论# 运行 ty_python_semantic 的全部 Markdown 类型推断测试 cargo test -p ty_python_semantic -- mdtest # 只运行本文讲解的 if_expression.md cargo test -p ty_python_semantic --test mdtest -- conditional/if_expression.md也可以使用仓库自带的mdtest.py运行器支持文件变更监听与 Rust 代码改动后自动重编译详见 crates/ty_python_semantic/mdtest.py 与 crates/ty_test/README.mduv run crates/ty_python_semantic/mdtest.py若需要按测试名过滤或自动更新内联快照可借助环境变量例如MDTEST_TEST_FILTERIf expressions cargo test -p ty_python_semantic --test mdtest小结通过 if_expression.md 这一组可执行规格可以完整看到 Ruff ty 类型检查器在条件表达式上的精确性追求分支类型合并真值未知时结果为分支联合含字面量精度短路驱动的分支锁定静态恒真/恒假条件直接落到单一分支且这一结论在嵌套布尔运算、比较链中依然成立保存与直接测试的语义差异保存后再次测试可能引入第二次__bool__调用从而破坏单一分支的保证Never传播返回Never别名的调用、受Never约束的类型变量都会让对应分支不可达错误建模布尔转换失败时产生unsupported-bool-conversion诊断并回退到兜底真值。理解这些规则不仅有助于读懂 ty 对if表达式的推断输出也能帮助你利用同一套测试框架为任意条件相关代码编写可回归的类型推断测试。若想进一步探索可继续阅读同目录的 if_statement.md、match.md以及更完整的短路语义矩阵 boolean/short_circuit.md。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表