拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ruff Ty 类型检查器中的 Eager Scopes:作用域求值时机与惰性粘滞语义详解

Ruff Ty 类型检查器中的 Eager Scopes作用域求值时机与惰性粘滞语义详解【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文深入解析 Ruff 项目 ty 类型检查器位于crates/ty_python_semantic中“急切作用域Eager Scopes”的核心语义类定义、推导式等作用域在定义时立即解析外层变量引用而函数定义、lambda 与类型别名则在调用时才解析。全文以规格文档 eager.md 为主干逐一剖析函数、类、列表/集合/字典推导式、生成器表达式、顶层作用域、注解延迟求值以及“惰性作用域粘性”规则并对照 scope.rs 中的ScopeKind::laziness()实现与 place_load.rs 中的快照解析源码让你既能读懂这些 mdtest 用例的每一处revealed:断言也能理解类型检查器在底层是如何按作用域类别决定解析时机的。读完本文你将掌握 ty 类型检查器作用域模型的完整行为表并能在实际 Python 类型推断中准确预判“外层变量何时被立即绑定、何时被延迟到调用时”。一、什么是 Eager Scopes定义与核心判据在 Python 语义中不同作用域对外层变量的引用存在两种解析时机急切eager解析作用域被执行求值时立即解析对外层作用域变量的引用绑定到那一刻可见的绑定惰性lazy解析引用被延迟到作用域真正被调用/使用时才解析此时外层变量的最终绑定可能已经改变。ty 类型检查器把这一区分建模为每个作用域的“惰性程度laziness”在 scope.rs 中通过ScopeLaziness枚举与ScopeKind::laziness()方法明确定义pub(crate) enum ScopeLaziness { /// The scope is evaluated lazily (e.g. function, type alias scope). Lazy, /// The scope is evaluated eagerly (e.g. module, class, comprehension scope). Eager, } impl ScopeKind { pub(crate) const fn laziness(self) - ScopeLaziness { match self { ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension | ScopeKind::TypeParams ScopeLaziness::Eager, ScopeKind::Function | ScopeKind::Lambda | ScopeKind::TypeAlias ScopeLaziness::Lazy, } } }从源码结构可以提炼出 eager/lazy 作用域的分类表作用域类型惰性程度说明Module模块Eager模块体从上到下顺序执行Class类体Eager类体在定义时执行Comprehension推导式Eager列表/集合/字典推导式在书写位置立即求值TypeParams类型参数作用域Eager但 bounds/constraints 延迟求值见下文Function函数体Lazy调用时才执行LambdalambdaLazy调用时才执行TypeAlias类型别名Lazy别名体延迟展开在 place_load.rs 中源码对 eager 作用域的快照语义有一段关键注释“An eager nested scope uses the global snapshot captured when it began, so a class body cannot see a module binding created only after that body finishes.”一个嵌套的 eager 作用域使用其开始时捕获的全局快照因此类体无法看到在该类体结束之后才创建的模块级绑定。这正是下文所有revealed:断言背后的统一原理eager 作用域捕获的是“进入作用域那一刻”的外层绑定快照。二、函数定义典型惰性求值函数体是惰性作用域即使外层变量在定义后被重新赋值函数体内部的引用也要到调用时才解析因此最终看到的是所有可达绑定这里即Literal[1, 2]。x 1 def f(): reveal_type(x) # revealed: Literal[1, 2] x 2reveal_type(x)在类型检查器中会输出一条RevealedType诊断参见 builder.rs 中reveal_type的内建处理逻辑mdtest 快照据此断言推断出的类型为Literal[1, 2]——因为惰性解析会收集x在函数可见范围内的全部赋值。三、类定义急切求值类体在定义时立即执行因此类体内的引用在类定义开始处就被急切解析只能看到此刻已有的绑定def _(): x 1 class A: reveal_type(x) # revealed: Literal[1] y x x 2 reveal_type(A.y) # revealed: int注意两个关键差异类体内reveal_type(x)解析为Literal[1]因为它发生在x 2之前且类是 eager 作用域只捕获进入类体时的快照A.y是类属性其值在类定义时就已确定为x 1的int所以类外reveal_type(A.y)得到的不是Literal[1]而是更宽的int——类属性的类型在绑定发生时就已定型不再跟随外层x变化。四、推导式列表、集合、字典全部急切求值三类推导式comprehension都是 eager 作用域在书写位置立即求值def _(): x 1 # revealed: Literal[1] [reveal_type(x) for a in range(1)] x 2def _(): x 1 # revealed: Literal[1] {reveal_type(x) for a in range(1)} x 2def _(): x 1 # revealed: Literal[1] {a: reveal_type(x) for a in range(1)} x 2三个例子结构完全对称推导式在x 1之后、x 2之前执行因此捕获到的x只有Literal[1]这一种绑定。这验证了 scope.rs 中Comprehension ScopeLaziness::Eager的实现。五、生成器表达式通常急切但存在“延迟执行”例外生成器表达式generator expression在规范上并不必然立即求值但实践中几乎总是立即被消费因此 ty 类型检查器默认按 eager 处理是更好的默认值def _(): x 1 # revealed: Literal[1] list(reveal_type(x) for a in range(1)) x 2这里生成器被立即传给list()消费等价于急切求值所以reveal_type(x)得到Literal[1]。5.1 未被立即消费时的“错误”推断当生成器表达式被保存下来、延迟到之后才迭代时eager 假设会产生与运行时不一致的结果def evaluated_later(): x 1 # revealed: Literal[1] y (reveal_type(x) for a in range(1)) x 2 # The generator isnt evaluated until here, so at runtime, x will evaluate to 2, contradicting # our inferred type. print(next(y))此时y只是被赋值、并未被迭代生成器体真正求值发生在next(y)处运行时x已经是 2而类型检查器按 eager 假设在定义处就解析为Literal[1]——注释明确承认这与运行时结果相矛盾。这是该文档承认的“已知不精确”场景。5.2 最左for子句的 iterable 表达式总是立即求值但存在一个关键的精确场景根据 Python 语言规范生成器表达式最左侧for子句中的 iterable 表达式会被立即求值eager.md 中引用的是 Python 语言参考手册关于生成器表达式的条文本文以文档语义为准不提供外部链接。因此即使生成器体本身延迟执行其首个 iterable 的求值时机仍然是 eager 的def iterable_evaluated_eagerly(): x 1 # revealed: Literal[1] y (a for a in [reveal_type(x)]) x 2 # Even though the generator isnt evaluated until here, the first iterable was evaluated # immediately, so our inferred type is correct. print(next(y))[reveal_type(x)]作为最左for子句的 iterable 立即求值捕获x 1因而最终推断Literal[1]是正确的——这与 5.1 中生成器体变量引用的场景形成了精确对比iterable 表达式 eager、生成器体变量按 eager 默认处理但运行时可能延迟。六、顶层模块级Eager Scopes上述所有行为在 eager 作用域直接嵌套于全局作用域时完全一致。但顶层例子还额外展示了另一个重要行为eager 作用域不能看到其结束后才创建的绑定“不可前向引用”。6.1 顶层类定义x 1 class A: reveal_type(x) # revealed: Literal[1] y x x 2 reveal_type(A.y) # revealed: int类体在x 1与x 2之间执行只看到Literal[1]A.y定型为int。6.2 顶层推导式同时验证后向绑定不可见列表、集合、字典推导式在顶层的行为完全一致且都验证了 eager 作用域“看不见其后定义的变量”x 1 # revealed: Literal[1] [reveal_type(x) for a in range(1)] x 2 # error: [unresolved-reference] [y for a in range(1)] y 1x 1 # revealed: Literal[1] {reveal_type(x) for a in range(1)} x 2 # error: [unresolved-reference] {y for a in range(1)} y 1x 1 # revealed: Literal[1] {a: reveal_type(x) for a in range(1)} x 2 # error: [unresolved-reference] {a: y for a in range(1)} y 1三者的要点相同推导式求值时y尚未绑定y 1在其后因此产生unresolved-reference错误——eager 作用域不会等待未来的绑定。这也与 place_load.rs 的快照注释相互印证eager 作用域使用的是“开始时”的快照。6.3 顶层生成器表达式x 1 # revealed: Literal[1] list(reveal_type(x) for a in range(1)) x 2 # error: [unresolved-reference] list(y for a in range(1)) y 1立即消费的生成器按 eager 处理得到Literal[1]而y未定义在生成器之前报unresolved-reference。对应的两个延迟场景文件文档中分别以evaluated_later.py、iterable_evaluated_eagerly.py命名x 1 # revealed: Literal[1] y (reveal_type(x) for a in range(1)) x 2 # The generator isnt evaluated until here, so at runtime, x will evaluate to 2, contradicting # our inferred type. print(next(y))x 1 # revealed: Literal[1] y (a for a in [reveal_type(x)]) x 2 # Even though the generator isnt evaluated until here, the first iterable was evaluated # immediately, so our inferred type is correct. print(next(y))七、核心规则Lazy Scopes 是“粘性”的当类型检查器向外逐层搜索引用的绑定/定义时只要沿途遇到任意一个 lazy 作用域整个查找就变为惰性——即使该 lazy 作用域之外还有 eager 作用域。这一“粘性sticky”规则决定了所有嵌套组合的解析结果。下面五种组合完整覆盖了该规则的行为空间。7.1 Eager 作用域内嵌 Eager 作用域保持急切未遇到 lazy 作用域时查找保持 eager且解析到的绑定不必在紧邻的外层作用域中。下例中列表推导式与类定义都是 eagerx立即被解析为唯一的x 1绑定def _(): x 1 class A: # revealed: Literal[1] [reveal_type(x) for a in range(1)] x 27.2 类体绑定对嵌套作用域不可见类定义虽是 eager 作用域但其内部产生的绑定明确不对任何嵌套作用域可见这些嵌套作用域通常是惰性的函数定义但该规则同样适用于推导式、其他类定义等嵌套 eager 作用域def _(): x 1 class A: x 4 # revealed: Literal[1] [reveal_type(x) for a in range(1)] class B: # revealed: Literal[1] [reveal_type(x) for a in range(1)] x 2 x 1 def _(): class C: # revealed: Literal[1] [reveal_type(x) for _ in [1]] x 2即使A内部已经绑定了x 4嵌套在A中的推导式仍然解析到外层函数_中的x 1而非A.x嵌套类B同理类C中x 2定义在推导式之后推导式仍解析到全局x 1。这印证了 implicit_attributes.rs 中处理类体属性可见性时对 eager 作用域父链的向上查找逻辑。7.3 Eager 作用域位于 Lazy 作用域之内变惰性列表推导式本身是 eager 作用域但它被包在惰性的函数定义中。由于在遇到任何绑定/定义之前必须先穿过这个 lazy 作用域查找变为惰性def _(): x 1 def f(): # revealed: Literal[1, 2] [reveal_type(x) for a in range(1)] x 2推导式最终看到x的全部可达绑定Literal[1, 2]而不是急切场景下的Literal[1]——惰性“传染”给了原本 eager 的推导式。7.4 Lazy 作用域位于 Eager 作用域之内仍然惰性函数定义是 lazy 作用域它被包在 eager 的类定义中。即便先穿过了 eager 作用域由于在遇到绑定/定义前穿过了 lazy 作用域查找依旧保持惰性def _(): x 1 class A: def f(): # revealed: Literal[1, 2] reveal_type(x) x 27.5 Lazy 作用域套 Lazy 作用域层层惰性无论穿过多少个 lazy 作用域才遇到绑定/定义查找始终保持惰性def _(): x 1 def f(): def g(): # revealed: Literal[1, 2] reveal_type(x) x 27.6 组合场景Eager 内嵌 Lazy 内嵌 Eager综合场景列表推导式eager被函数定义lazy包裹再被类定义eager包裹。虽然最后穿过的作用域是 eager 的但由于途中遇到了 lazy 作用域整个查找仍是惰性的def _(): x 1 class A: def f(): # revealed: Literal[1, 2] [reveal_type(x) for a in range(1)] x 2粘性规则一句话总结查找过程中的“第一个 lazy 作用域”决定一切——它之前的 eager 作用域无关紧要之后的查找全部惰性化。八、注解Annotations延迟求值的边界类型注解有时会被延迟求值deferred。一旦延迟注解中引用的类型即使在 eager 作用域中也会被惰性查找。这解释了为什么同一个类属性在不同注解模式下会得到不同结果。8.1 Python 文件中的急切注解默认无from __future__ import annotations情况下注解立即求值类属性var捕获x int时的类型from typing import ClassVar x int class C: var: ClassVar[x] reveal_type(C.var) # revealed: int x str8.2 Python 文件中的延迟注解启用from __future__ import annotations后注解字符串被延迟求值引用以惰性方式解析最终合并了int与str两种可能from __future__ import annotations from typing import ClassVar x int class C: var: ClassVar[x] reveal_type(C.var) # revealed: int | str x str8.3 Stub 文件.pyi中的延迟注解Stub 文件中的注解默认就是延迟的但当前实现存在一处已知偏差文档中以 TODO 标注from typing import ClassVar x int class C: var: ClassVar[x] # TODO: should ideally be str, but we currently consider all reachable bindings reveal_type(C.var) # revealed: int | str x str理想情况下应为str因为 stub 文件不参与运行时赋值顺序但当前实现仍考虑所有可达绑定因此给出int | str。这是文档明确声明的实现局限读者应将其视为当前行为而非最终规范。8.4 注解作用域Annotation Scopes以下用例通过[environment]配置指定python-version 3.12见 eager.md 内嵌的 TOML 配置块用于启用 3.12 的 PEP 695 语法type别名与[T]类型参数[environment] python-version 3.12类型别名type alias的注解作用域是惰性的。type Foo Bar在Bar定义之前声明但别名体延迟求值因此后续能正确解析到Bar并完成类型收窄type Foo Bar class Bar: pass def _(x: Foo): if isinstance(x, Bar): reveal_type(x) # revealed: Bar else: reveal_type(x) # revealed: Never类型参数作用域type-param scope是 eager 的但其 bounds/constraints 延迟求值。基类列表中的类型引用与函数参数注解中的类型引用按 eager 解析因此在Bar定义之前引用会报unresolved-reference而 bound 约束T: Bar可以引用后定义的Bar# error: [unresolved-reference] class DT: pass class E[T: Bar]: pass # error: [unresolved-reference] def gT: pass def hT: Bar: pass class Bar: pass这正对应 scope.rs 中ScopeKind::TypeParams ScopeLaziness::Eager的实现类型参数作用域本身急切但注解内容按前文规则走延迟路径。九、如何运行这些用例mdtest 测试体系eager.md属于 ty 类型检查器的mdtest文档驱动测试体系。mdtest 会解析 Markdown 文件中的代码块把revealed:、error: [rule]等断言与类型检查诊断逐一比对。运行方式参见 mdtest.py测试套件目录通过MDTEST_SUITES注册mdtest 目录为crates/ty_python_semantic/resources/mdtest脚本会先编译mdtest可执行文件通过cargo test --testmdtest探测产物见 mdtest.py再以--exact test_function::markdown_file精确运行单个文档的用例环境变量INSTA_FORCE_PASS1、INSTA_OUTPUTnone控制快照断言MDTEST_EXTERNAL、MDTEST_UPDATE_SNAPSHOTS等控制外部依赖与快照更新行为。你可以把eager.md中任意一个代码块独立抽出作为理解 ty 类型检查器“该作用域到底怎么解析”的最小复现样本修改revealed:行后重新运行 mdtest即可观察类型推断结果的变化。断言比对的具体实现位于crates/mdtest/src/assertion.rs与crates/mdtest/src/matcher.rsmdtest crate 的src目录下reveal_type的诊断输出则在 builder.rs 的report_revealed_type调用链中产生。十、总结Eager Scopes 行为速查作用域场景解析时机结果特征函数体 / lambda / 类型别名Lazy收集全部可达绑定如Literal[1, 2]类体Eager捕获进入类体时的快照如Literal[1]列表/集合/字典推导式Eager立即解析书写位置的绑定生成器表达式立即消费默认 Eager按 eager 推断通常正确生成器表达式延迟迭代实际运行时 Lazyeager 假设可能与运行时矛盾最左for的 iterable 表达式总是 Eager推断精确Eager 内嵌 EagerEager解析最近可用的 eager 绑定类体绑定对嵌套作用域不可见嵌套作用域跳过类内绑定途经任一 Lazy 作用域整体 Lazy粘性一旦遇 lazy查找即惰性化延迟注解 / stub 注解Lazy引用合并多种可能类型TypeParams 作用域Eagerbounds 延迟基类/参数注解 eagerbound 可引用后定义类型这套语义是 ty 类型检查器准确推断 Python 程序类型的基础之一其权威行为定义即 eager.md源码级佐证可继续查阅 scope.rs、place_load.rs 与 mdtest.py。理解“惰性粘性”与“eager 快照”这两个核心概念后你就能精确预判任意嵌套作用域下reveal_type的输出。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门