尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入 ty 类型检查器:`type[Any]` 与动态 `type[]` 语义剖析(基于 ty_python_semantic mdtest 实测)

深入 ty 类型检查器:`type[Any]` 与动态 `type[]` 语义剖析(基于 ty_python_semantic mdtest 实测) 深入 ty 类型检查器type[Any]与动态type[]语义剖析基于 ty_python_semantic mdtest 实测【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/rufftype[X]是 Python 类型标注中用来描述类对象的类型构造形式meta-type但当类型参数是Any、Unknown或直接使用裸type时其语义充满了容易踩坑的细节。本文以 Ruff 仓库中 ty 类型检查器的官方行为测试文档 crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md 为骨架逐条剖析type[Any]、type[Unknown]、裸type与type[object]之间的类型推断与可赋值性规则并给出完整的可运行测试用例。读完本文你将掌握 ty 对动态类对象类型的精确处理策略并能在本地用 mdtest 工具链复现每一个revealed与error断言。从一篇 Markdown 说起mdtest 是什么在深入了解type[Any]之前先解释这些测试用例的运行载体。dynamic.md位于crates/ty_python_semantic/resources/mdtest/type_of/目录下属于 ty 类型检查器的Markdown 测试套件mdtestMarkdown 文件本身就是测试文件中的py代码块会被提取出来做类型推断与类型检查行内注释则扮演断言。根据 crates/ty_python_semantic/resources/README.mdmdtest/子目录下的 Markdown 文件是类型推断与类型检查的测试由tests/mdtest.rs集成测试执行。具体解析逻辑位于 crates/mdtest/src/parser.rs其中每个##小节Section对应一个测试用例无名 TOML 代码块充当该小节的配置例如[environment] python-version 3.13py/pyi等代码块被拼接为嵌入的 Python 文件并参与检查。断言语法由 crates/mdtest/src/assertion.rs 定义支持两类核心注释# revealed: SomeType—— 断言reveal_type(...)推断出的类型# error: [rule-code] message—— 断言产生某个诊断错误方括号内是错误码双引号内是可选的错误消息片段。例如def f(x: type[Any], y: type[str]): reveal_type(x) # revealed: type[Any] a: type[str] xdynamic.md的文档头明确说明其测试对象是非完全静态的type[]类型即type[Any]与type[Unknown]。需要提醒的是ty 是一个独立的类型检查器实现其行为基于 typing 规范但存在自己的取舍以下所有结论均以当前仓库代码与测试为准。type[Any]最基本的行为先看最简单的场景## Simple小节。type[Any]表示某个未知类的类对象from typing import Any def f(x: type[Any], y: type[str]): reveal_type(x) # revealed: type[Any] # TODO: could be object.__repr__ type Any reveal_type(x.__repr__) # revealed: Any # type[str] and type[Any] are assignable to each other a: type[str] x b: type[Any] y class A: ... x: type[Any] object x: type[Any] type x: type[Any] A x: type[Any] A() # error: [invalid-assignment]这里透露出三个重要规则type[Any]的推断结果就是type[Any]。它不会退化为更具体的类字面量class literal类型type[Any]与type[str]可以互相赋值。因为Any代表未知集合ty 采取动态类型放行策略任何type[X]都能赋给type[Any]反之type[Any]也能赋给具体的type[str]这是Any与具体类型双向兼容性的体现type[Any]只能接收类对象。object、type、自定义类A都可以赋值但类的实例A()不行会触发[invalid-assignment]错误。这说明尽管参数是Anytype[]仍然严格约束必须是类对象这一运行时形态。一个值得注意的细节x.__repr__的推断结果是Any而非某个绑定方法类型。测试注释中留有 TODO指出理论上它可以是object.__repr__ type Any的交集类型但当前实现直接给出Any体现 ty 对动态类型采取尽快转向动态、不做过深推断的工程取舍。裸type等价于type[object]## Bare type小节处理了一个在规范层面有争议的问题裸type注解到底怎么解释The interpretation of baretypeis not clear: existing wording in the spec does not match the behavior of mypy or pyright. For now we interpret it as simply an instance ofbuiltins.type, which is equivalent totype[object]. This is similar to the current behavior of mypy, and pyright in strict mode.即ty 将裸type解释为builtins.type的一个实例等价于type[object]。这与 mypy 以及 pyright strict 模式下的行为一致。测试用例def f(x: type): reveal_type(x) # revealed: type reveal_type(x.__repr__) # revealed: bound method type.__repr__() - str class A: ... x: type object x: type type x: type A x: type A() # error: [invalid-assignment]与type[Any]的关键差异立即显现reveal_type(x)的结果是type即builtins.type的类字面量x.__repr__则能精确推断出bound method type.__repr__() - str。也就是说裸type被当作完全静态的具体类型处理成员访问走的是type类自身的元类协议而不是退化到Any。赋值规则与type[Any]一致类对象object、type、A可以赋值实例A()报[invalid-assignment]。type[object]≠type[Any]这是dynamic.md专门辟出一节强调的边界直观地证明了带Any的动态类对象类型与以object为参数的具体类对象类型不可混为一谈def f(x: type[object]): reveal_type(x) # revealed: type reveal_type(x.__repr__) # revealed: bound method type.__repr__() - str class A: ... x: type[object] object x: type[object] type x: type[object] A x: type[object] A() # error: [invalid-assignment]注意这里的两个结果reveal_type(x)显示为type而不是type[object]。从类型系统的角度看type[object]所描述的值集合恰好就是所有类对象即builtins.type的实例因此 ty 把它归一化为typex.__repr__的推断同样是精确的bound method type.__repr__() - str。对比第一节的type[Any]同样是x.__repr__type[Any]给出Any而type[object]给出具体绑定方法。差异的根源在于Any在 ty 的类型格中代表未知类型集合任何对该类型值的属性访问都保持动态而type[object]的参数object是确定的具体类型其元类属性可以直接通过builtins.type的方法签名解析出来。Any的__class__是type[Any]而不是Any## The type ofAnyistype[Any] 一节揭示了 ty 对动态值的类对象最精妙的处理。先看原文推理Anyrepresents an unknown set of possible runtime values. Ifxis of typeAny, the type ofx.__class__is also unknown and remains dynamic,exceptthat we know it must be a class object of some kind. As such, the type ofx.__class__istype[Any]rather thanAny.翻译过来Any是未知运行时值的集合。若x是Any那么x.__class__的类型同样是未知的、保持动态——但我们知道它必然是某种类的类对象。因此x.__class__的类型被判定为type[Any]而不是Any。这里type[]外壳保留了这是类对象这一结构性事实同时用Any参数标记其具体身份未知。测试用例from typing import Any from does_not_exist import SomethingUnknown # error: [unresolved-import] reveal_type(SomethingUnknown) # revealed: Unknown def test(x: Any, y: SomethingUnknown): reveal_type(x.__class__) # revealed: type[Any] reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Any] reveal_type(y.__class__) # revealed: type[Unknown] reveal_type(y.__class__.__class__.__class__.__class__) # revealed: type[Unknown]两个值得展开的细节__class__的链式访问保持稳定。x.__class__.__class__.__class__.__class__依然是type[Any]。这符合元类语义type的__class__是type本身类对象套类对象依然收敛为类对象因此type[Any]在链式__class__访问下保持不变不会发散出其他类型形态Unknown与Any在此行为一致。from does_not_exist import SomethingUnknown因[unresolved-import]产生错误其类型被推断为Unknownty 对未解析名称的占位类型而y.__class__被推断为type[Unknown]链式访问同样保持type[Unknown]。type[Unknown]与type[Any]的相似性最后一节##type[Unknown]has similar properties totype[Any] 进一步确认type[Unknown]拥有与type[Any]几乎一致的动态放行性质并测试了type[]类型向type子类实例的赋值import abc from typing import Any from does_not_exist import SomethingUnknown # error: [unresolved-import] has_unknown_type SomethingUnknown.__class__ reveal_type(has_unknown_type) # revealed: type[Unknown] def test(x: type[str], y: type[Any]): Both type[Any] and type[Unknown] are assignable to all type[] types a: type[Any] x b: type[str] y c: type[Any] has_unknown_type d: type[str] has_unknown_type def test2(a: type[Any]): type[Any] and type[Unknown] are also assignable to all instances of type subclasses b: abc.ABCMeta a b: abc.ABCMeta has_unknown_type两个要点双向全通的可赋值性。type[Any]与type[Unknown]既可以赋给type[Any]也可以赋给具体的type[str]。也就是说对于任何type[X]目标type[Any]/type[Unknown]都是兼容的源类型可赋值给type子类的实例。abc.ABCMeta是type的元类子类type[Any]与type[Unknown]都能赋给abc.ABCMeta类型的变量。这符合type[]的元类语义类对象的类型应能归入其元类类型。从源码看实现class literal 与元类收敛上面这些行为在 ty 的实现中并非零散特判而是由类字面量类型class literal与元类归一化机制支撑的。搜索 crates/ty_python_semantic/src 可以发现大量与class_literal相关的逻辑例如 place.rs 中的try_to_class_literalcrates/ty_python_semantic/src/place.rs#L2298附近以及 semantic_model.rs 中通过ty.is_class_literal()断言类字面量类型的场景。从测试行为可以推断出 ty 的两级表示策略静态类对象裸type、type[object]、type[A]归一化为具体的类字面量类型因此成员访问如__repr__能解析出bound method type.__repr__() - str这类精确签名动态类对象type[Any]、type[Unknown]保留type[]外壳并用动态类型作参数成员访问直接转向动态Any但是类对象这一结构性约束始终存在故赋值目标仍限定为类对象或元类类型。这种设计同时解释了type[object]的 reveal 结果是typetype[object]的值集合与type类字面量完全重合ty 选择归一化而非保留冗余形式。而type[Any]之所以不归一化为type是因为Any的未知性一旦丢失就无法再支撑可赋给任何type[X]的动态放行语义。在本地运行与验证这些测试如果你想亲自验证上述每个revealed与error断言可以使用仓库自带的 Markdown 测试运行器 crates/ty_python_semantic/mdtest.py。它支持传入过滤器定位到具体文件例如针对本文讨论的测试套件python crates/ty_python_semantic/mdtest.py type_of/dynamic.md运行器会先用cargo test --package ty_python_semantic --testmdtest编译测试再执行匹配的 Markdown 测试。mdtest.py还支持以下参数--enable-external/-e启用带外部依赖的测试--no-lockfile-upgrades当 Markdown 测试中的依赖需求变化时默认会自动升级 lockfile此参数可禁用--no-snapshot-updates禁用过期内联快照的自动更新。更便捷的是其watch 模式不传过滤器直接运行会进入文件监听状态监视 Rust 源码、vendored typeshed 与 Markdown 测试文件的变化一旦有改动便自动重新编译并重跑相关测试。这意味着当你修改类型推断逻辑时dynamic.md中每一个# revealed:注释都是一条活的回归测试。延伸阅读type[]主题在 mdtest 目录下还有三个姊妹文件分别覆盖不同侧面可与本文对照阅读crates/ty_python_semantic/resources/mdtest/type_of/basic.md类字面量、嵌套类、跨模块类字面量、新旧式联合、字符串化注解、非法参数与final类等基础与边界场景crates/ty_python_semantic/resources/mdtest/type_of/generics.mdtype[T]带类型变量的行为包括无界/有界 TypeVar 的构造签名检查与可调用性crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.mdtyping.Type这一别名形式的相关测试。结合 crates/mdtest/src/parser.rs 与 crates/mdtest/src/assertion.rs 理解测试格式本身你就能把这些 Markdown 文件当作可执行的类型系统规范来使用——这正是 ty 项目用文档驱动类型检查器正确性的核心实践。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表