ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Ruff 类型检查器(ty):`types.ModuleType` 隐式模块全局变量的类型推断规则详解

2026/9/11 0:13:25 拓冰建站 浏览量
Ruff 类型检查器(ty):`types.ModuleType` 隐式模块全局变量的类型推断规则详解 Ruff 类型检查器tytypes.ModuleType隐式模块全局变量的类型推断规则详解【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读在 Python 中每个模块都隐式拥有__name__、__file__、__doc__等特殊属性但类型检查器如何得知这些名字的类型本篇文章以 Ruff 仓库内 ty 类型检查器的行为测试文档 moduletype_attrs.md 为主线结合 types.rs 与 place.rs 的实现系统讲解types.ModuleType隐式全局变量的推断规则、排除项、覆盖/遮蔽规则与模块字面量module literal属性访问语义。读完本文你将掌握 ty 检查器在名字查找失败时回退到ModuleType属性这一机制上的完整行为边界包括__doc__的特化、__getattr__/__dict__/__init__的特殊处理以及条件定义下的联合类型推断。说明mdtest是 ty 类型检查器使用的一套 Markdown 驱动测试格式本文所引用的代码块均为可直接运行的测试用例其中的reveal_type(...)输出与# error: [诊断码]注释即为检查器给出的真实结论。隐式ModuleType全局变量名字查找的最后一道回退ty 推断器的核心设计前提是所有模块都是types.ModuleType的实例。因此当一个名字在局部作用域与全局作用域中都找不到时ty 不会立刻判定其为未绑定unbound而是先到 typeshed 中types.ModuleType的实例属性上再查一次只有ModuleType上也没有该属性时才认定名字未定义。这一逻辑在 place.rs 的注释中有明确阐述全局查找失败后会回退到types.ModuleType或builtins.object的实例成员而module_type_implicit_global_symbol函数place.rs则专门负责模块全局命名空间中的隐式符号解析其文档注释直接点名__doc__、__name__、__file__这类符号因为类型恒定我们直接把它们当作types.ModuleType的实例属性来查找。标准隐式属性及推断结果下面的测试用例展示了 ty 对常见ModuleType属性的推断结论reveal_type(__name__) # revealed: str # Typeshed says this is str | None, but for a pure-Python on-disk module its always str reveal_type(__file__) # revealed: str reveal_type(__loader__) # revealed: LoaderProtocol | None reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None reveal_type(__path__) # revealed: MutableSequence[str] reveal_type(__builtins__) # revealed: Any # error: [possibly-unresolved-reference] Name __warningregistry__ used when possibly not defined reveal_type(__warningregistry__) # revealed: dict[Any, int]几个值得注意的细节__name__为str与 typeshed 的声明一致没有特殊化。__file__恒为strtypeshed 中__file__的类型是str | None但 ty 判断磁盘上的纯 Python 模块其__file__永远是字符串。这是通过 place.rs 的特例分支实现的代码注释明确写道对于一个 Python 模块内部的隐式全局查找它永远是str尽管 typeshed 说是str | None。__warningregistry__是可能未定义它由 warnings 机制懒创建可能不存在。因此在源码中它被标记为Definedness::PossiblyUndefinedplace.rs从而产生possibly-unresolved-reference诊断——这是 ty 为了避免假阴性而故意为之的建模方式。这些隐式属性在任何嵌套作用域内同样可用因为它们总是能在全局作用域中回退到import sys reveal_type(sys.__builtins__) # revealed: Any from builtins import __builtins__ as __bi__ reveal_type(__bi__) # revealed: Any class X: reveal_type(__name__) # revealed: str def foo(): reveal_type(__name__) # revealed: strsys.__builtins__与from builtins import __builtins__都推断为Any与模块内部__builtins__一致说明该名字的三条路径模块内隐式全局、sys属性、builtins模块导出类型统一。三个被排除的隐式属性并非types.ModuleType上的所有属性都会变成模块的隐式全局变量。以下三个属性被显式排除模块内直接引用会触发unresolved-reference结果类型Unknown# error: [unresolved-reference] # revealed: Unknown reveal_type(__getattr__) # error: [unresolved-reference] # revealed: Unknown reveal_type(__dict__) # error: [unresolved-reference] # revealed: Unknown reveal_type(__init__)对应实现位于module_type_symbolsplace.rs 附近返回的符号列表会显式过滤掉__dict__ | __getattr__ | __init__。这样设计的原因是这些属性要么属于ModuleType的实例机制而非模块命名空间__dict__、__init__要么属于 typeshed 为动态导入而伪造的钩子__getattr__把它们当作普通全局变量会让用户写出无法运行的代码。__doc__反映模块的实际 docstring特化到strtypeshed 将types.ModuleType.__doc__标注为str | None。但如果模块带有一个字面量 docstring运行时__doc__必然被设置为该字符串除非以-OO模式运行ty 选择忽略这种可能性。此时 ty 会把__doc__收窄为strSome docstring reveal_type(__doc__) # revealed: str源码中的实现位于 place.rs当module_docstring(...)存在时__doc__直接绑定到str实例否则回退到 typeshed 的str | None。注释解释了取舍——docstring 会在-OO优化模式下被剥离但这里我们假设实际存在 docstring 且使用了__doc__就足以相信它在运行时存在。也就是说ty 采用只要写了 docstring 就按str处理的实用主义策略。与显式赋值/声明的交互覆盖与遮蔽规则隐式ModuleType属性可以在全局作用域被覆盖但类型必须可赋值给ModuleType上的声明除非伴随显式重声明。看module.py的完整用例__file__ None __path__: list[str] [] __doc__: int # error: [invalid-declaration] Cannot declare type int for inferred type str | None # error: [invalid-declaration] Cannot shadow implicit global attribute __package__ with declaration of type int __package__: int 42 __spec__ 42 # error: [invalid-assignment] Object of type Literal[42] is not assignable to ModuleSpec | None逐条解读__file__ None合法None可赋值给str | None对磁盘模块 ty 虽推断为str但显式覆盖仍须以 typeshed 声明str | None为基准做可赋值性检查。__path__: list[str] []合法list[str]可赋值给MutableSequence[str]。__doc__: int报错不能对推断类型为str | None的隐式属性声明int触发invalid-declaration。__package__: int 42报错声明类型int遮蔽了隐式全局属性__package__str | None同样触发invalid-declaration。__spec__ 42报错Literal[42]不可赋值给ModuleSpec | None触发invalid-assignment。从外部导入该模块时看到的是各名字最终生效的类型覆盖后的结果出错的名字回退为Unknown或原类型import module reveal_type(module.__file__) # revealed: None reveal_type(module.__path__) # revealed: list[str] reveal_type(module.__doc__) # revealed: Unknown reveal_type(module.__spec__) # revealed: ModuleSpec | None # error: [unresolved-attribute] reveal_type(module.__warningregistry__) # revealed: Unknown注意module.__doc__显示为Unknown因为声明报错、module.__spec__回退为 typeshed 的ModuleSpec | None赋值被拒而module.__warningregistry__对外部文件不可见访问触发unresolved-attribute。隐式属性同样参与嵌套作用域与global关键字交互def nested_scope(): global __loader__ reveal_type(__loader__) # revealed: LoaderProtocol | None __loader__ 56 # error: [invalid-assignment] Object of type Literal[56] is not assignable to LoaderProtocol | Noneglobal __loader__声明将赋值导向模块全局随后__loader__ 56因不满足LoaderProtocol | None而报invalid-assignment。模块字面量类型上的属性访问ModuleType与object成员皆可见当通过导入拿到一个具体的模块即模块字面量类型ModuleLiteral时属性访问的规则与模块内部查找不同__dict__、__init__以及所有builtins.object上的属性都可以作为属性访问尽管它们在模块内部无法作为全局变量使用。命名空间包namespace package同样适用。import typing import namespace_package reveal_type(typing.__name__) # revealed: str reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None ...) - None # Note that since the source for the typing module is a stub file, # we cant know for sure that its not a C extension at runtime, # and C extensions dont necessarily have a __file__ global attribute # at all (in which case this attribute access would fail). However, we # *do* know that typing is not a namespace package, so if __file__ # does exist, it will be of type str (__file__ is only None for # namespace packages). reveal_type(typing.__file__) # revealed: str # These come from builtins.object, not types.ModuleType: reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) - bool reveal_type(typing.__class__) # revealed: class ModuleType reveal_type(typing.__dict__) # revealed: dict[str, Any] reveal_type(namespace_package.__name__) # revealed: str reveal_type(namespace_package.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None ...) - None reveal_type(namespace_package.__file__) # revealed: None reveal_type(namespace_package.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) - bool reveal_type(namespace_package.__class__) # revealed: class ModuleType reveal_type(namespace_package.__dict__) # revealed: dict[str, Any]实现依据在 place.rs 的注释中作为属性查找时__init__在模块上可用但模块内部作为全局不可用builtins.object的成员也一并可用因为ModuleType继承自object。另有一个细节typing模块源码是 stub 文件无法确认其运行时是否为 C 扩展C 扩展可能根本没有__file__属性但 ty 知道它不是命名空间包因此只要__file__存在就是str而命名空间包的__file__恒为None。typeshed 的伪造__getattr__模块字面量上被忽略ModuleType实例上可用typeshed 在types.ModuleType的 stub 中放置了一个伪造的__getattr__用于支持动态导入。ty 对确切知道是哪个模块的模块字面量类型会忽略它import typing # error: [unresolved-attribute] reveal_type(typing.__getattr__) # revealed: Unknown但当我们面对一个通用的ModuleType实例时__getattr__恢复可用任意属性访问都被允许且结果为Anyimport types reveal_type(types.ModuleType.__getattr__) # revealed: def __getattr__(self, name: str) - Any def f(module: types.ModuleType): reveal_type(module.__getattr__) # revealed: bound method ModuleType.__getattr__(name: str) - Any reveal_type(module.__all__) # revealed: Any reveal_type(module.whatever) # revealed: Any源码层面place.rs 在回退查找时对__getattr__直接返回Place::Undefined并在注释中解释typeshed 的伪造__getattr__是为动态导入准备的对于确切知道模块身份的ModuleLiteral类型我们不应使用它。而ModuleType实例的成员查找走MemberLookupPolicy::NO_GETATTR_LOOKUP之外的常规路径因此保留__getattr__的Any语义。types.ModuleType.__dict__优先于全局变量__dict__模块内部无法覆盖ModuleType实例的__dict__属性ty 会优先采用 stub 中的属性而不是模块全局命名空间里名为__dict__的变量。但模块内部对__dict__的直接引用仍然解析到全局变量本身foo.py__dict__ foo reveal_type(__dict__) # revealed: Literal[foo]bar.pyimport foo from foo import __dict__ as foo_dict reveal_type(foo.__dict__) # revealed: dict[str, Any] reveal_type(foo_dict) # revealed: dict[str, Any]这一行为与static_member的实现一致types.rs注释明确写道__dict__是一个非常特殊的成员永远不会被模块全局变量覆盖我们应始终直接在types.ModuleType上查找它绝不在模块的全局作用域中查找。因此模块内foo.py自洽的__dict__引用与模块外foo.__dict__属性访问解析到完全不同的实体——这正是同名的全局变量与实例属性分道扬镳的典型案例。条件定义联合类型与模块属性优先级无条件覆盖模块命名空间优先在模块命名空间中覆盖的属性优先于ModuleType属性__file__ foo def returns_bool() - bool: return True if returns_bool(): __name__ 1 # error: [invalid-assignment] Object of type Literal[1] is not assignable to str reveal_type(__file__) # revealed: Literal[foo] reveal_type(__name__) # revealed: str__file__ foo无条件覆盖后模块内__file__恒为Literal[foo]。而__name__的赋值发生在条件分支内因此最终类型取条件定义类型与ModuleType属性类型的联合。由于Literal[1]不可赋值给str该赋值本身报invalid-assignment而reveal_type(__name__)仍显示str——这是因为类型检查时条件分支中合法的赋值路径此处为空与隐式属性类型合并后保持str。带注解的条件定义显式联合同样的规则适用于带注解的名字。此时联合类型会如实呈现为两个类型的并集# error: [invalid-declaration] Cannot shadow implicit global attribute __file__ with declaration of type int __file__: int 42 def returns_bool() - bool: return True if returns_bool(): # error: [invalid-declaration] Cannot shadow implicit global attribute __name__ with declaration of type int __name__: int 1 reveal_type(__file__) # revealed: Literal[42] reveal_type(__name__) # revealed: Literal[1] | str注意两个错误都来自声明的类型不能遮蔽隐式全局属性这一规则模块级__file__: int因int与str | None不符而报invalid-declaration模块命名空间已覆盖Literal[42]生效条件分支内的__name__: int 1同样报invalid-declaration但由于它是条件性的最终__name__类型为Literal[1] | str——即条件定义类型与隐式ModuleType属性的联合。这正是文档标题所说的Conditionally global orModuleTypeattribute, with annotation语义条件定义不会完全遮蔽隐式属性而是与其求并集。隐式全局属性遮蔽 builtins 中的同名符号模块的隐式__name__全局变量优先级高于 builtins 命名空间中的同名符号。下面这个用例通过自定义 typeshed[environment] typeshed /typeshed构造了一个builtins 中也声明了__name__: int的环境main.py中的配置与 stub[environment] typeshed /typeshed/typeshed/stdlib/builtins.pyi:class object: ... class tuple: ... class int: ... class bytes: ... __name__: int 42/typeshed/stdlib/types.pyi:class ModuleType: __name__: bytes/typeshed/stdlib/typing_extensions.pyi:def reveal_type(obj, /): ...main.py:reveal_type(__name__) # revealed: bytes结果__name__的类型取自定义 typeshed 中ModuleType.__name__声明的bytes而不是 builtins 中的int。原因正如文档所总结main模块拥有一个隐式的__name__全局符号它遮蔽了 builtin 的同名符号。这也从侧面印证了 builtins 回退如 place.rs 的implicit_builtins_symbol只会发生在普通全局查找失败之后而ModuleType隐式属性优先于 builtins 参与解析。小结一套自洽的模块特殊属性解析协议将上述规则汇总ty 对模块特殊属性的处理可归纳为三层协议场景查找/推断结果模块内引用__name__、__loader__等回退到ModuleType实例属性typeshed 类型模块内引用__file__恒为str特例绕过 typeshed 的str | None模块带字面量 docstring 时引用__doc__收窄为str模块内引用__getattr__/__dict__/__init__unresolved-reference结果为Unknown显式排除模块内引用__warningregistry__dict[Any, int]且标记可能未定义模块字面量上访问__init__/__dict__/object成员全部可用ModuleType实例属性语义模块字面量上访问__getattr__unresolved-attribute忽略 typeshed 伪造钩子ModuleType实例上访问任意属性Any__getattr__生效支持动态导入全局显式覆盖隐式属性必须可赋值给 typeshed 声明否则invalid-assignment/invalid-declaration条件性覆盖隐式属性与隐式属性类型求并集如Literal[1] | str__dict__同名冲突模块外属性访问始终取ModuleType的dict[str, Any]模块内取全局变量隐式属性 vs builtins 同名符号隐式ModuleType属性优先这套机制的核心目的是在精准性如__file__、__doc__的特化与宽容性如ModuleType实例的__getattr__兜底、__warningregistry__的可能未定义建模之间取得平衡让动态的 Python 模块语义在静态类型检查下既足够安全、又不至于误报。对于想深入理解 ty 检查器模块系统设计的开发者本文引用的 moduletype_attrs.md 测试文档与 place.rs、types.rs 源码是相互印证的第一手资料mdtest中 scopes 目录下的 global.md、builtin.md 等文件还覆盖了全局作用域与 builtins 的更多边界情形可作为延伸阅读。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考