news 2026/9/12 3:48:51

深入 Ruff 内置类型检查器 ty:`Any` 动态类型与渐进类型系统的语义解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 Ruff 内置类型检查器 ty:`Any` 动态类型与渐进类型系统的语义解析

深入 Ruff 内置类型检查器 ty:Any动态类型与渐进类型系统的语义解析

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

Any是 Python 渐进类型系统(gradual type system)中的动态类型,它不像object那样描述"所有对象的集合",而是描述一个未知的值集合。本文以 Ruff 仓库中ty类型检查器(crates/ty_python_semantic)的可执行规范文档 type_compendium/any.md 为主体,系统讲解AnyobjectNever、并集、交集、元组之间的类型语义,并对照源码说明这些语义在类型检查器中的实现与验证方式。读完本文,你将理解Any的双向可分配性、Any | T的"下界"语义、Any & T的"上界"语义,以及tuple[int, Any]tuple[int, object]的本质区别,并能读懂ty中以静态断言形式书写的类型系统测试。

背景:渐进类型系统与Any的定位

ty的渐进类型系统中,Any被定义为一个动态类型:它不代表某个具体的静态类型,而代表一个未知的静态类型,即一个未知的运行时值集合。这与object(所有对象的静态上界,Top)、Never(没有任何值的下界,Bottom)形成对照:

  • object描述的集合是已知且固定的:全体 Python 对象;
  • Never描述的集合是已知且固定的:空集;
  • Any描述的集合是未知的:它可能对应任意某个具体的静态类型所描述的值集合。

在源码层面,ty内部正是把Any表示为"动态类型"枚举的一员。在 crates/ty_python_semantic/src/types.rs 中,Type的构造器any()直接返回Type::Dynamic(DynamicType::Any)

pub(crate) const fn any() -> Self { Self::Dynamic(DynamicType::Any) }

DynamicType枚举中明确注释了Any的来源——types.rs:

/// An explicitly annotated `typing.Any` Any,

Any同族的动态类型还包括Unknown(未标注注解时的"未知类型",见 type_compendium 相关的 ty_extensions 文档)以及Todo等。值得注意的一点是,types.rs 的is_spellable实现指出,Unknown@Todo虽是非标准扩展,但它们在语义上都精确等价于Any,可以在用户注解中书写:

// `Unknown` and `@Todo` are nonstandard extensions, // but they are both exactly equivalent to `Any` Type::Dynamic(_) => true,

可执行的类型语义:mdtest、static_assert与类型谓词

本文依据的核心文档位于 crates/ty_python_semantic/resources/mdtest/type_compendium/any.md,它是tymdtest体系中的一份"可执行规范":文档中的 Python 代码片段会被当作真实测试运行,其中标注的# revealed:# error:# snapshot:等注释都会被编译器逐条校验。该类文档的测试运行环境由文档开头的 TOML 配置指定:

[environment] python-version = "3.14"

即本节内容均在仓库所支持的 Python 3.14 环境配置下验证。

验证Any语义依赖三个核心工具(其完整说明见 ty_extensions.md):

  • ty_extensions.static_assert(expr):在类型检查期强制断言某个表达式具有静态已知的真值。若参数求值为False,或真值性无法静态确定,则报出static-assert-error诊断;断言失败时还会在诊断中显示Inferred type of argument is ...,给出参数被推断出的类型。
  • ty_extensions._internal.is_assignable_to(S, T):类型谓词,判断类型S是否可以赋给类型T,返回Literal[True]Literal[False]
  • ty_extensions._internal.is_equivalent_to(S, T):判断两个类型是否等价,同样返回字面量布尔类型。

is_assignable_to的基础用法为例(同样出自 ty_extensions.md 的 "Assignability" 小节):

from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to from typing import Any static_assert(is_assignable_to(int, Any)) static_assert(is_assignable_to(Any, str)) static_assert(not is_assignable_to(int, str))

由于这些谓词返回的是Literal[True]/Literal[False]static_assert便能在编译期对任意类型关系进行断言,构成一份可回归、可执行的类型系统行为规格。

核心规则一:Any的双向可分配性

Any最显著的性质是:每个类型都可以赋给Any,同时Any也可以赋给每个类型。它既是所有类型的"超类型",也是所有类型的"子类型",这正是"动态类型"的含义——因为Any代表的未知集合可能恰好就是目标类型对应的集合,也可能恰好是源类型对应的集合。

在 any.md 中,这一性质通过如下断言被固化:

from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to from typing_extensions import Never, Any class C: ... static_assert(is_assignable_to(C, Any)) static_assert(is_assignable_to(Any, C)) static_assert(is_assignable_to(object, Any)) static_assert(is_assignable_to(Any, object)) static_assert(is_assignable_to(Never, Any)) static_assert(is_assignable_to(Any, Never)) static_assert(is_assignable_to(type, Any)) static_assert(is_assignable_to(Any, type)) static_assert(is_assignable_to(type[Any], Any)) static_assert(is_assignable_to(Any, type[Any]))

逐条解读:

  • 普通类CAny双向可赋值;
  • object(静态 Top)与Any双向可赋值——注意这意味着二者等价,Any是未知集合,object是全体对象集合,二者的可赋值性来自Any的双向动态语义,而"等价"是更强的概念;
  • Never(静态 Bottom)与Any双向可赋值;
  • 类字面量类型typeAny、以及type[Any]Any双向可赋值。

此外,与所有类型一样,Any也具有自反性:

static_assert(is_assignable_to(Any, Any))

在实现层面,这种"双向放行"来自tyType::Dynamic(_)类型在处理赋值(is_assignable_to)时的特殊分支。is_assignable_toType上的核心关系查询,被赋值检查(types/diagnostic.rs 中的AssignmentDiagnosticKind)、调用绑定(types/call/bind.rs)、约束求解(types/constraints.rs)等大量内部逻辑引用;当任意一侧是动态类型时,检查会直接放行,从而在源码层面对应文档断言的行为。

核心规则二:与并集的交互Any | T—— 未知下界

Any与一个完全静态的类型T取并集,得到Any | T。文档给出的语义是:它描述了一个至少与T一样大的未知值集合,即它代表一个下界为T的未知完全静态类型。

用可赋值关系可以精确地演示这一点。考虑类层级Small <: Medium <: Big

from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to, is_equivalent_to from typing_extensions import Any # A class hierarchy Small <: Medium <: Big class Big: ... class Medium(Big): ... class Small(Medium): ... static_assert(is_assignable_to(Any | Medium, Big)) static_assert(is_assignable_to(Any | Medium, Medium)) # `Any | Medium` is at least as large as `Medium`, so we cannot assign it to `Small`: static_assert(not is_assignable_to(Any | Medium, Small))

由于Any | Medium代表的未知类型至少Medium(可能更大),它可以赋给Medium及其超类Big;但它不可能被收窄为Small,因为未知集合可能取到Medium本身(甚至更大的类型),所以"赋给Small"无法被静态保证。

一个值得注意的退化情形:Any | object等价于object。事实上任何类型与object取并集都等价于object(因为object已是静态全集),此处只是借Any再次验证:

static_assert(is_equivalent_to(Any | object, object)) static_assert(is_equivalent_to(object | Any, object))

也就是说,当未知下界顶到静态上界object时,未知性被"吸收",并集退化为一个确定的全集类型。关于ty中并集类型的整体处理,可进一步阅读 union_types.md。

核心规则三:与交集的交互Any & T—— 未知上界

Any与完全静态类型T取交集,得到Any & T。语义与并集对称:它描述了一个不大于T的未知值集合,即一个上界为T的未知完全静态类型。

沿用同样的类层级:

from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to, is_equivalent_to from typing import Any class Big: ... class Medium(Big): ... class Small(Medium): ... static_assert(is_assignable_to(Small, Any & Medium)) static_assert(is_assignable_to(Medium, Any & Medium))

SmallMedium都可以赋给Any & Medium,因为该交集代表的未知类型最多Medium,而SmallMedium都满足这个上界约束。反过来:

static_assert(not is_assignable_to(Big, Any & Medium))

Big不能赋给Any & Medium——Any & Medium不可能比Medium更大,因此不存在任何使交集"大到容纳Big"的实例化(materialization)方式。文档中对此的表述是:不存在Any & Medium的实例化能使其与Big一样大。

交集还有一个重要的退化情形:Any & Never等价于Never。因为Never是唯一一个"不大于Never"的完全静态类型,未知上界一旦收紧到Never,未知集合就只能取空集本身:

from typing_extensions import Never static_assert(is_equivalent_to(Any & Never, Never)) static_assert(is_equivalent_to(Never & Any, Never))

可见Any与静态类型的并/交运算,恰好把"未知性"限制在了一个由静态类型T围出的区间内:Any | T给出下界,Any & T给出上界。关于交集类型的构造与化简(例如Intersection[int, bool]化简为boolIntersection[int, Never]化简为Never),可参阅 ty_extensions.md 的 "Intersection" 小节与 intersection_types.md。

核心规则四:元组中的Anytuple[int, Any]tuple[int, object]

Any嵌入复合类型后,最能体现"未知集合"与"已知集合"差异的场景是元组。类型系统概念文档(关于渐进类型的章节)指出:

tuple[int, Any]并不代表"所有首元素为整数的二元组"这一单个集合——那是一个完全静态类型,应该写作tuple[int, object]。相反,tuple[int, Any]代表某个未知的二元组值集合:它可能是所有二元整数组的集合,可能是"整数 + 字符串"二元组的集合,也可能是其它某个二元组集合。

这一区别在实践中的可观测差异是:可以把tuple[int, Any]类型的表达式赋给tuple[int, int]类型的目标,而把tuple[int, object]赋给tuple[int, int]则是静态类型错误:

from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to from typing import Any static_assert(is_assignable_to(tuple[int, Any], tuple[int, int])) static_assert(not is_assignable_to(tuple[int, object], tuple[int, int]))

原因在于:tuple[int, Any]的第二个元素是"未知类型",该未知集合可能恰好int,因此目标tuple[int, int]是它的一个可能实例化,赋值被允许;而tuple[int, object]的第二个元素是确定的全集object,把全体对象的集合收窄为int在静态上是无法保证的,因此报错。这解释了为何Any在复合类型内部与object行为截然不同——object是"已知的大",Any是"未知",而"未知"恰恰为赋值保留了可能性。

源码印证:Anyty内部表示与书写规则

除了Type::Dynamic(DynamicType::Any)这一核心表示外,源码中还有几处与本文主题直接相关的实现细节:

  • 类型定义回溯:在 types.rs 中,Type::definition会把内部的动态Any映射回用户可书写的特殊形式SpecialFormType::Any,即运行时typing.Any对应的特殊形式,保证诊断与reveal_type输出使用用户熟悉的拼写:
Self::Dynamic(DynamicType::Any) => { Type::SpecialForm(SpecialFormType::Any).definition(db, env) }
  • 拼写识别typing.Any是命名Any类型的方式;即使把它别名重命名(如from typing import Any as RenamedAny),ty仍识别为Any;而用户自定义的class Any: ...则只是普通类,不再是动态类型。这部分行为由 annotations/any.md 以reveal_type断言形式固化。
  • 允许子类化:类型规范允许定义Any的子类。直接或间接子类的实例保留其名义类型与声明的成员(reveal_type(SubclassOfAny())显示为SubclassOfAny),其 MRO 中会插入Any(如(<class 'SubclassOfAny'>, Any, <class 'object'>))。这与Unknown同样可被子类化的行为一致,见 ty_extensions.md。
  • 物化(materialization)机制:渐进类型系统在处理未知动态类型时,会通过"物化"将其解析为静态上界或下界。源码注释中提到Top[list[Any]]Bottom[list[Any]]这类带MaterializationKindGenericAlias表示(types.rs):以Top物化的list[Any]是所有list[Any]实例化的上界,Bottom物化则对应下界。这正是"未知集合被静态类型围出区间"这一直觉在实现中的体现——并集Any | T给出下界、交集Any & T给出上界,而物化负责在这些边界之间做静态化处理。

总结

Any是 Ruff 内置类型检查器ty渐进类型系统的基石:它代表一个未知的静态类型,因此与所有类型双向可赋值;与静态类型取并集时表现为"下界为T的未知类型"(Any | T),取交集时表现为"上界为T的未知类型"(Any & T);嵌入元组后,tuple[int, Any]tuple[int, object]展现出截然不同的可赋值行为——前者是"可能的实例化",后者是"确定的大集合"。

这些语义并非停留在文档叙述层面,而是被 type_compendium/any.md 中以static_assert+is_assignable_to/is_equivalent_to的形式固化为可执行测试,并在 types.rs 中以Dynamic(DynamicType::Any)为核心表示、配合特殊形式映射与物化机制落地。如果想继续深入,可以对照阅读同目录下的 never.md、object.md,以及类型属性测试 is_assignable_to.md 与 is_equivalent_to.md,从而完整掌握ty对渐进类型语义的建模方式。

【免费下载链接】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 3:46:17

ReVanced Manager 的 Downloader 怎么安装并在设置中授予信任

ReVanced Manager 的 Downloader 怎么安装并在设置中授予信任 【免费下载链接】revanced-manager &#x1f48a; Application to use ReVanced on Android 项目地址: https://gitcode.com/GitHub_Trending/re/revanced-manager Downloader 是 ReVanced Manager 用来下载…

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

国产MCU替代STM32的五大隐蔽坑:从时钟到烧录的实战避坑指南

/* 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 3:45:32

基于Python的名中医肿瘤治疗教学案例库:从数据清洗到用药规律挖掘

我去年带学生做过一个基于Python的名中医肿瘤治疗教学案例库设计与实现。说实话&#xff0c;刚拿到这个题目时我心里是有点嘀咕的——"案例库"听起来不就是增删改查吗&#xff1f;但真正做进去才发现&#xff0c;这块地儿说不上多难啃&#xff0c;但坑是真不少。从病…

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

ARM信创桌面开发效率工具选型指南

/* 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 3:42:54

隐私协议与用户协议撰写指南及合规实践

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

作者头像 李华