Ruff ty 类型检查器中的typing.Any完整语义:从基本用法到 Any 子类的渐进赋值机制
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
typing.Any是 Python 类型系统中的特殊存在:任何类型都可以赋值给它,而它也可以赋值给任何类型。本文以 Ruff 仓库中 ty 类型检查器的规格测试文档 annotations/any.md 为主体,逐条剖析 ty 对Any的建模与实现——包括拼写识别、别名与遮蔽、Any子类的单向渐进赋值、运行时属性,以及isinstance/issubclass等非法用法。读完本文,你将掌握 ty 中Any的完整行为边界,并能看懂reveal_type、reveal_mro等调试指令的输出,以及各类[invalid-*]诊断的触发条件。
背景:mdtest 规格文档与 ty 的对应关系
本文讨论的文档位于crates/ty_python_semantic/resources/mdtest/annotations/any.md,它是 ty 类型检查器(Ruff 仓库中独立于 linter/formatter 的类型检查引擎,源码位于 crates/ty_python_semantic)的 mdtest 规格测试文件。这类文档采用"文档即测试"的形式:每个代码块都会被实际执行,其中形如reveal_type(x) # revealed: Any的注释是断言,# error: [invalid-assignment]则是期望产生的诊断,# snapshot用于断言完整的多行诊断输出。
ty 对Any的核心建模分散在以下几处源码中,后续小节会逐一引用:
- special_form.rs:
SpecialFormType::Any是typing.Any这个特殊形式的类型表示,例如Self::Any => Some(Type::any())(第 223 行)表明在合法的类型表达式中,Any特殊形式会被转换为动态类型Type::any(); - class_base.rs:
ClassBase::Any与ClassBase::Dynamic(DynamicType::Any)两个变体,分别表示"显式继承自Any"与"基类表达式的类型是Any/Unknown",这是本文"子类"章节的底层实现; - diagnostic.rs:
invalid-assignment、invalid-type-form、call-non-callable、invalid-metaclass、invalid-argument-type等诊断的定义位置。
Any的拼写与识别:Annotation
typing.Any是命名Any类型的一种方式。最基本的用法是作为变量注解,此时该变量可以被重新赋值为任意类型,且在函数内部reveal_type始终揭示为Any:
from typing import Any x: Any = 1 x = "foo" def f(): reveal_type(x) # revealed: Any关键点在于:reveal_type揭示的是变量的声明类型而非当前值,因此即使x被先后赋值为int和str,其类型依然稳定地是Any。这体现了Any的"动态"本质——ty 不会对它做任何细化或跟踪。
别名也生效:Aliased to a different name
Any的类型身份绑定在符号上,而非名字上。即使把typing.Any导入成别的名字,ty 依然能识别它是Any的另一种拼写:
from typing import Any as RenamedAny x: RenamedAny = 1 x = "foo" def f(): reveal_type(x) # revealed: Any从实现上看,这是通过在模块属性解析时把符号解析为SpecialFormType::Any(见 special_form.rs 的try_from_file_and_name,它会按导入路径匹配 special form),再经由Self::Any => Some(Type::any())(第 223 行)得到动态类型,与变量名完全无关。
名字遮蔽:Shadowed class
反过来,如果用户在自己的模块里定义了一个名为Any的类,那么类型表达式中出现的Any指向的是这个本地类,而不是typing.Any。mdtest 用一个小技巧验证了这一点:
class Any: ... x: Any def f(): reveal_type(x) # revealed: Any # 由于 str 可以赋给 typing.Any,但不可赋给本地定义的类, # 下面的赋值必须报错,以此证明我们没有误用 typing.Any。 y: Any = "not an Any" # error: [invalid-assignment]这里的reveal_type(x) # revealed: Any揭示的是本地类Any的实例类型(诊断渲染时同名显示),而y: Any = "not an Any"会触发[invalid-assignment]:因为str与本地空类Any不兼容。这正是 mdtest 断言机制的巧妙之处——通过"反向验证"排除实现中意外回退到typing.Any的可能。
Any的子类:渐进赋值(gradual assignability)的核心
规范允许定义Any的子类,这也是整个文档中信息量最大的部分。ty 的行为可以概括为一条单向规则:
直接或间接继承自
Any的类,其实例保留自身的名义类型(nominal type)与已声明的成员,但可以渐进地赋值给任意类型;反过来,任意值不能赋给这种子类。
直接与间接子类
from typing import Any from ty_extensions._internal import reveal_mro class SubclassOfAny(Any): ... class IndirectSubclass(SubclassOfAny): ... reveal_mro(SubclassOfAny) # revealed: (<class 'SubclassOfAny'>, Any, <class 'object'>) reveal_mro(IndirectSubclass) # revealed: (<class 'IndirectSubclass'>, <class 'SubclassOfAny'>, Any, <class 'object'>) reveal_type(SubclassOfAny()) # revealed: SubclassOfAny reveal_type(IndirectSubclass()) # revealed: IndirectSubclass not_a_direct_instance: SubclassOfAny = 1 # error: [invalid-assignment] not_an_indirect_instance: IndirectSubclass = 1 # error: [invalid-assignment] direct_as_int: int = SubclassOfAny() indirect_as_int: int = IndirectSubclass()reveal_mro显示Any会作为一个动态元素出现在 MRO 中,而reveal_type则证明实例的推断类型仍是具体的子类(并非被抹成Any)。赋值的单向性非常明确:int赋给SubclassOfAny报[invalid-assignment],而SubclassOfAny()赋给int合法。
从源码看,这个行为由 class_base.rs 中的ClassBase::Any变体支撑:try_from_explicit_base(第 120-132 行)专门处理"基类表达式是SpecialFormType::Any"的情况,将其保留为Self::Any而非走通用的try_from_type转换。
final 类同样适用
即使是@final标记的类,Any子类的实例依然可以赋给它们:
from typing import final @final class FinalClass: ... f: FinalClass = SubclassOfAny() @final class OtherFinalClass: ... f: FinalClass | OtherFinalClass = SubclassOfAny()注意最后一行:Any子类实例还能赋给联合类型(union),哪怕联合的每个成员都是 final 类。这说明渐进赋值的粒度比"单个类"更细——只要赋值目标不是"封闭"得过于严格(见下文"字面量类型"),都可通过。
动态创建的子类
type("B", (A,), {})动态创建的类,以及以它为基类的普通类,行为保持一致:
from typing import Any, final from ty_extensions._internal import reveal_mro class A(Any): ... B = type("B", (A,), {}) class C(B): ... @final class FinalClass: ... reveal_mro(B) # revealed: (<class 'B'>, <class 'A'>, Any, <class 'object'>) reveal_mro(C) # revealed: (<class 'C'>, <class 'B'>, <class 'A'>, Any, <class 'object'>) b: FinalClass = B() c: FinalClass = C()Annotated元数据不影响行为
基类上的Annotated元数据(如class B(Annotated[A, "metadata"]))只是静态元数据,运行时 MRO 中并不存在,因此渐进赋值行为完全不变:
from typing import Annotated, Any, Literal, final from ty_extensions._internal import reveal_mro class A(Any): ... class B(Annotated[A, "metadata"]): ... @final class FinalClass: ... reveal_mro(B) # revealed: (<class 'B'>, <class 'A'>, Any, <class 'object'>) x: FinalClass = B() y: Literal[1] = B()泛型别名基类
当Any子类再继承泛型别名时(GenericSubclass[int]),渐进赋值依然被保留:
from typing import Any, Generic, Literal, TypeVar, final T = TypeVar("T") class GenericSubclass(Any, Generic[T]): ... class SubclassOfGenericAlias(GenericSubclass[int]): ... @final class FinalClass: ... final: FinalClass = SubclassOfGenericAlias() literal: Literal[1] = SubclassOfGenericAlias()边界情况一:基类表达式的类型是Any(而非显式继承Any)
文档强调了一个容易混淆的细节:基类表达式的推断类型是Any不等于显式继承Any。例如在函数中class DynamicBase(base)(base: Any)——此时动态 MRO 元素让实例可以赋给非 final 类,但不能赋给 final 类或字面量类型,这与显式Any基类形成对比:
from typing import Any, Literal, final class Arbitrary: ... @final class FinalClass: ... def check_dynamic_base(base: Any): class DynamicBase(base): ... class IndirectSubclass(DynamicBase): ... reveal_type(DynamicBase()) # revealed: DynamicBase ordinary: Arbitrary = IndirectSubclass() final: FinalClass = DynamicBase() # error: [invalid-assignment] literal: Literal[1] = DynamicBase() # error: [invalid-assignment] indirect_final: FinalClass = IndirectSubclass() # error: [invalid-assignment] indirect_literal: Literal[1] = IndirectSubclass() # error: [invalid-assignment]在 class_base.rs 中,这一分支对应ClassBase::Dynamic(DynamicType::Any)(第 299 行),与ClassBase::Any是两个独立变体——这正是该边界能被精确区分的原因。
边界情况二:基类表达式的类型是Unknown
同理,继承自推断类型为Unknown的名字(例如从无法解析的模块导入的类),实例可以赋给非 final 类,但不能赋给 final 类:
from typing import final from somewhere import UnknownBase # error: [unresolved-import] class Arbitrary: ... @final class FinalClass: ... class FromUnknownBase(UnknownBase): ... reveal_type(FromUnknownBase()) # revealed: FromUnknownBase ordinary_unknown: Arbitrary = FromUnknownBase() final_unknown: FinalClass = FromUnknownBase() # error: [invalid-assignment]这里[unresolved-import]与[invalid-assignment]同时出现,恰好演示了 ty 在"无法解析"场景下的降级策略:ClassBase::unknown()(ClassBase::Dynamic(DynamicType::Unknown),见 class_base.rs 第 46-47 行)提供有限度的兼容性,但不会像显式Any基类那样赋予完全的渐进赋值能力。
Callable与Protocol赋值
Any子类的实例还可以赋给任意Callable和Protocol类型:
from typing import Callable, Any, Protocol def takes_callable1(f: Callable[..., Any]): f() takes_callable1(SubclassOfAny()) def takes_callable2(f: Callable[[int], None]): f(1) takes_callable2(SubclassOfAny()) class CallbackProtocol(Protocol): def __call__(self, x: int, /) -> None: ... def takes_callback_proto(f: CallbackProtocol): f(1) takes_callback_proto(SubclassOfAny()) class OtherProtocol(Protocol): x: int @property def foo(self) -> bytes: ... @foo.setter def foo(self, x: str) -> None: ... def takes_other_protocol(f: OtherProtocol): ... takes_other_protocol(SubclassOfAny())失败的结构检查错误上下文会被丢弃
当赋值目标是一个联合类型,其中某个元素因显式Any基类而赋值成功时,ty 会丢弃来自失败结构检查的错误上下文,而不是报出误导性的错误:
from typing import Any, Callable class CallableSubclassOfAny(Any): def __call__(self, x: int) -> str: raise NotImplementedError class IncompatibleCallable: def __call__(self, x: int) -> bytes: raise NotImplementedError def check_callable_union(value1: CallableSubclassOfAny | IncompatibleCallable): target1: Callable[[int], int] = value1 # snapshot此时 snapshot 断言输出的诊断只指向真正不兼容的联合元素IncompatibleCallable(返回类型bytes不能赋给int),而不会把CallableSubclassOfAny一并标记为失败:
error[invalid-assignment]: Object of type `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` --> src/mdtest_snippet.py:141:37 | 141 | target1: Callable[[int], int] = value1 # snapshot | -------------------- ^^^^^^ Incompatible value of type `CallableSubclassOfAny | IncompatibleCallable` | | | Declared type info: element `IncompatibleCallable` of union `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` info: └── type `IncompatibleCallable` has inferred callable type `(x: int) -> bytes` info: └── incompatible return types: `bytes` is not assignable to `int`字面量类型(Literal)赋值
Any子类实例还可以通过其实例类型的"动态元素"赋给字面量类型:
from typing import Any, Literal class MockAny(Any): pass x: Literal[1] = MockAny()真实使用场景:mock 库
文档明确指出,Any子类最常见的真实场景是测试 mock 库——mock 对象应当可以赋给(几乎)任何类型。标准库unittest.mock.MagicMock就利用了这一点:
from unittest.mock import MagicMock x: int = MagicMock()这意味着 ty 对MagicMock的建模(作为Any的隐式子类或等效处理)允许它赋给任意声明类型,这正是 mock 模式在类型检查下依然可用的关键。
运行时属性:Runtime properties
ty 对Any运行时行为的建模:
- 在 Python 3.11+ 上,
typing.Any在运行时是一个类;typing_extensions.Any则始终是类; - 在更早的 Python 版本上,
typing.Any是typing._SpecialForm的实例,但 ty目前不建模这种差异——在所有 Python 版本上,ty 都推断Any拥有类应有的全部属性:
from typing import Any from ty_extensions import static_assert from ty_extensions._internal import TypeOf, is_assignable_to reveal_type(Any.__base__) # revealed: type | None reveal_type(Any.__bases__) # revealed: tuple[type, ...] static_assert(is_assignable_to(TypeOf[Any], type))这与 special_form.rs 第 180 行的映射一致:Self::Any被归为KnownClass::Type,即把Any视为type类的实例来处理属性访问。
非法用法:Invalid
Any有四类在运行时必然抛错的用法,ty 都会给出对应的诊断。
不能被参数化
from typing import Any # error: [invalid-type-form] "Special form `typing.Any` expected no type parameter" def f(x: Any[int]): reveal_type(x) # revealed: UnknownAny[int]会触发[invalid-type-form]诊断(定义于 diagnostic.rs 的invalid-type-form,第 729 行附近),且此时x的类型被降级为Unknown。
不能被调用
Any() # error: [call-non-callable] "Object of type `<special-form 'typing.Any'>` is not callable"不能用作元类
元类在底层会隐式调用Any,因此同样报错:
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `<special-form 'typing.Any'>` is not callable"不能用于isinstance(),但可以用于issubclass()
isinstance(x, Any)在运行时抛TypeError,ty 报[invalid-argument-type];即使Any嵌套在元组里(包括非字面量元组)也一样:
# error: [invalid-argument-type] "`typing.Any` cannot be used with `isinstance()`: This call will raise `TypeError` at runtime" isinstance("", Any) isinstance("", (int, Any)) # error: [invalid-argument-type] isinstance("", (int, (str, Any))) # error: [invalid-argument-type] classes = (int, Any) isinstance("", classes) # error: [invalid-argument-type]而issubclass()检查是允许的(issubclass走类元层面的检查,不会触发同样的运行时错误):
issubclass(object, Any) # no error! issubclass(object, (int, Any)) # no error! issubclass(object, (int, (str, Any))) # no error!小结:一张表理解 ty 的Any行为
| 场景 | ty 的行为 | 诊断/输出 |
|---|---|---|
x: Any = 1; x = "foo" | 类型恒为Any | revealed: Any |
from typing import Any as RenamedAny | 别名仍识别为Any | revealed: Any |
本地定义class Any | 遮蔽typing.Any | 赋值str报[invalid-assignment] |
class Sub(Any) | 实例保留名义类型,可单向渐进赋值给任意类型 | MRO 中含Any元素;int不能赋给Sub |
动态基类(base: Any) | 实例可赋给非 final 类,不可赋给 final/Literal | 赋值 final 报[invalid-assignment] |
Unknown基类 | 同上,但可赋性进一步受限 | [unresolved-import]+[invalid-assignment] |
Any[int] | 拒绝参数化 | [invalid-type-form] |
Any() | 拒绝调用 | [call-non-callable] |
metaclass=Any | 拒绝作元类 | [invalid-metaclass] |
isinstance(x, Any) | 拒绝(含元组嵌套) | [invalid-argument-type] |
issubclass(x, Any) | 允许 | 无错误 |
如果你想在自己的项目里验证这些行为,可以直接阅读或运行 annotations/any.md 中的代码片段——每个代码块都是可执行的可断言测试;reveal_type、reveal_mro与# error:注释共同构成了 ty 对Any语义的可回归验证体系。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考