ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ruff ty 类型推断深入:`len()` 从字面量长度到 `__len__` 协议的全链路解析

Ruff ty 类型推断深入:`len()` 从字面量长度到 `__len__` 协议的全链路解析 Ruff ty 类型推断深入len()从字面量长度到__len__协议的全链路解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本篇技术指南以 Ruff 仓库中类型检查器ty的 mdtest 测试规范 len.md 为骨架完整讲解len()内置函数在静态类型推断中的行为字符串、字节串、元组字面量的精确长度如何被推断为Literal类型列表、集合、字典为何只能得到int以及__len__特殊方法的返回值、签名与Sized协议校验规则。读完本文你将掌握 ty 类型检查器对len()的完整推断规则并能在源码层面理解其底层实现Type::len、python_len、KnownFunction::Len绑定与测试运行机制。文档背景这不是普通教程而是可执行的类型测试规范该文档位于crates/ty_python_semantic/resources/mdtest/expression/目录下属于 Ruff ty 类型检查器的mdtest 测试套件。与普通文档不同这里面的每一段 Python 代码块都是可执行的测试用例reveal_type(...)是类型检查器的调试内置函数用于“揭示”某个表达式的静态类型# revealed: ...注释声明该表达式的期望类型测试运行器会据此断言# error: [invalid-argument-type] ...注释声明某一行必须产生指定的诊断错误码与消息。测试由仓库内的 crates/ruff_mdtest 驱动它解析 Markdown 中嵌入的py代码块在内存文件系统上构建类型检查数据库db.rs 中Db使用InMemorySystem每次测试重置为干净状态随后逐条运行断言。因此本文呈现的每一条规则都同时是该类型检查器可验证的实际行为而非纸面承诺。字符串与字节字面量长度被精确推断为Literal普通、raw 与拼接字面量reveal_type(len(no\rmal)) # revealed: Literal[6] reveal_type(len(raw stri\ng)) # revealed: Literal[10] reveal_type(len(rconca\t ena\tion)) # revealed: Literal[14] reveal_type(len(bytes lite rbal)) # revealed: Literal[11] reveal_type(len(©ℇ)) # revealed: Literal[7]逐条拆解表达式推理过程结果no\rmal普通字符串中\r是一个回车字符共n o \r m a l6 个字符Literal[6]raw stri\ngraw 字符串中\n是两个字面字符\与n共 10 个字符Literal[10]rconca\t ena\tion相邻字符串拼接raw 部分conca\t7 字符 普通部分ena\tion\t算 1 个制表符7 字符Literal[14]bytes lite rbal字节串拼接9 2Literal[11]©ℇUnicode 字符串按字符数计数7 个字符尽管 UTF-8 编码后占据更多字节Literal[7]最后一个用例揭示了一个关键实现细节StringLiteralType::python_len在 literal.rs 中实现为self.value(db).chars().count()即按Unicode 字符char计数而非字节数BytesLiteralType::python_len则直接使用self.value(db).len()literal.rs对字节串按字节计数。字符串字面量的__len__成员类型检查器不仅推断len(s)还精确推断字符串字面量自身的__len__方法类型reveal_type(abc.__len__) # revealed: () - Literal[3] reveal_type(babc.__len__) # revealed: () - Literal[3]从源码看在类成员查找class_member_with_policy_inner中对字符串/字节字面量类型的__len__成员做了专门分支types.rs直接合成一个签名为(self) - Literal[长度]的函数可调用类型参数为 position-only 的self返回类型是精确的字面量整数。因此abc.__len__被揭示为() - Literal[3]。多行字面量# fmt: off保护# fmt: off reveal_type(len( # revealed: Literal[7] foo bar )) reveal_type(len( # revealed: Literal[9] rfoo\r bar )) reveal_type(len( # revealed: Literal[7] bfoo bar )) reveal_type(len( # revealed: Literal[9] rbfoo\r bar )) # fmt: on多行字符串的内容包含真实换行符foo\nbar为foo 换行 bar 7而 raw 多行字符串中\r是字面反斜杠加字母r故rfoo\r\nbar为foo(3) \r(2 个字符) 换行(1) bar(3) 9。字节串b...与rb...同理。这里的# fmt: off/# fmt: on用于阻止 Ruff formatter 重排这些多行字面量的缩进从而保证测试输入与期望长度稳定一致。元组定长元组的长度是精确的变长元组只能回退到int字面量元组与解包reveal_type(len(())) # revealed: Literal[0] reveal_type(len((1,))) # revealed: Literal[1] reveal_type(len((1, 2))) # revealed: Literal[2] reveal_type(len(tuple())) # revealed: Literal[0] reveal_type(len((*[],))) # revealed: Literal[0]带星号解包的元组同样可以精确统计# fmt: off reveal_type(len( # revealed: Literal[1] ( *[], 1, ) )) # fmt: on reveal_type(len((*[], 1, 2))) # revealed: Literal[2] reveal_type(len((*[], *{}))) # revealed: Literal[0](*[], 1, 2)中空列表解包贡献 0 个元素加上1、2共 2 个(*[], *{})两边都为空长度为 0。这说明 ty 在解包splat场景下也能精确统计元组字面量的元素个数其底层基础是 tuple.rs 中定义的TupleLength枚举Fixed(usize)表示定长Variable(usize, usize)表示带未知长度同质段变长前缀/后缀之外的“开放”部分。元组子类与泛型参数class EmptyTupleSubclass(tuple[()]): ... class Length1TupleSubclass(tuple[int]): ... class Length2TupleSubclass(tuple[int, str]): ... class UnknownLengthTupleSubclass(tuple[int, ...]): ... reveal_type(len(EmptyTupleSubclass())) # revealed: Literal[0] reveal_type(len(Length1TupleSubclass((1,)))) # revealed: Literal[1] reveal_type(len(Length2TupleSubclass((1, foo)))) # revealed: Literal[2] reveal_type(len(UnknownLengthTupleSubclass((1, 2, 3)))) # revealed: int泛型实参为定长规格tuple[()]、tuple[int]、tuple[int, str]的子类其长度可精确推断一旦泛型实参含变长段tuple[int, ...]长度未知len()揭示为int。同样地定长元组的__len__方法类型也是精确的reveal_type(tuple[int, int].__len__) # revealed: (self: tuple[int, int], /) - Literal[2] reveal_type(tuple[int, ...].__len__) # revealed: (self: tuple[int, ...], /) - int def f(x: tuple[int, int], y: tuple[int, ...]): reveal_type(x.__len__) # revealed: () - Literal[2] reveal_type(y.__len__) # revealed: () - int reveal_type(EmptyTupleSubclass.__len__) # revealed: (self: tuple[()], /) - Literal[0] reveal_type(EmptyTupleSubclass().__len__) # revealed: () - Literal[0] reveal_type(UnknownLengthTupleSubclass.__len__) # revealed: (self: tuple[int, ...], /) - int reveal_type(UnknownLengthTupleSubclass().__len__) # revealed: () - int注意类class object上访问__len__时签名带self参数(self: tuple[int, int], /) - Literal[2]实例上访问则已绑定 self() - Literal[2]。覆盖__len__时优先采用覆盖后的返回类型from typing import Literal class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]): def __len__(self) - Literal[42]: return 42 reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42] class FixedLengthSubclassWithDunderLenOverridden(tuple[int]): def __len__(self) - Literal[42]: # error: [invalid-method-override] return 42 reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]两个要点用户自定义的__len__优先无论基类是变长元组还是定长元组len()都采用子类覆盖后的返回类型Literal[42]。定长元组子类覆盖__len__会触发invalid-method-override因为tuple[int]的__len__精确类型是Literal[1]子类将其改为Literal[42]与父类方法签名不一致违反方法覆盖规则但对tuple[int, ...]父类__len__为int的覆盖是合法的。列表、集合、字典无论内容如何len()始终是intreveal_type(len([])) # revealed: int reveal_type(len([1])) # revealed: int reveal_type(len([1, 2])) # revealed: int reveal_type(len([*{}, *dict()])) # revealed: int reveal_type(len({})) # revealed: int reveal_type(len({**{}})) # revealed: int reveal_type(len({**{}, **{}})) # revealed: int reveal_type(len({1})) # revealed: int reveal_type(len({1, 2})) # revealed: int reveal_type(len({*[], 2})) # revealed: int reveal_type(len(list())) # revealed: int reveal_type(len(set())) # revealed: int reveal_type(len(dict())) # revealed: int reveal_type(len(frozenset())) # revealed: int这是与字符串/元组最鲜明的对比列表、集合、字典即便在源码中显式给出了元素ty 也一律揭示为int。原因在于这类容器是运行时可变mutable的——元素可以后续被append、add、update修改长度并非字面量不变的常量因此类型系统不对它们做字面量长度推断直接回退到 typeshed 中len()的声明返回类型int。即使len([*{}, *dict()])中两个容器都是空的结果依然是int。__len__返回值语义隐式且递归地转换为intlen(x)在运行时等价于type(x).__len__(x)其返回值的类型处理有一条核心规则文档原文__len__的返回值会被隐式且递归地implicitly and recursively转换为int。在 types.rs 的Type::len实现中这一规则体现为non_negative_int_literal辅助函数fn non_negative_int_literaldb( db: db dyn Db, env: ProgramEnvironmentdb, ty: Typedb, ) - OptionTypedb { match ty { // TODO: Emit diagnostic for non-integers and negative integers Type::LiteralValue(literal) match literal.kind() { LiteralValueTypeKind::Int(value) (value.as_i64() 0).then_some(ty), LiteralValueTypeKind::Bool(value) Some(Type::int_literal(i64::from(value))), _ None, }, Type::Union(union) union.try_map(db, env, |element| { non_negative_int_literal(db, env, *element) }), _ None, } }可见三件事整数保持原样但仅当非负 0时才保留为精确字面量布尔值被转换为整数True → 1、False → 0联合类型被递归逐元素映射任何无法转换为非负整数的成员都会使整个结果回退。字面量整数与布尔值from typing import Literal class Zero: def __len__(self) - Literal[0]: return 0 class ZeroOrOne: def __len__(self) - Literal[0, 1]: return 0 class ZeroOrTrue: def __len__(self) - Literal[0, True]: return 0 class OneOrFalse: def __len__(self) - Literal[1] | Literal[False]: return 1 class OneOrFoo: def __len__(self) - Literal[1, foo]: return 1 class ZeroOrStr: def __len__(self) - Literal[0] | str: return 0 reveal_type(len(Zero())) # revealed: Literal[0] reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1] reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1] reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0] # error: [invalid-argument-type] Argument to function len is incorrect: Expected Sized, found OneOrFoo reveal_type(len(OneOrFoo())) # revealed: int # error: [invalid-argument-type] Argument to function len is incorrect: Expected Sized, found ZeroOrStr reveal_type(len(ZeroOrStr())) # revealed: int行为归纳Literal[0]、Literal[0, 1]原样保留Literal[0, True]中True被归一化为1结果为Literal[0, 1]Literal[1] | Literal[False]中False归一化为0且联合被规范化为Literal[1, 0]注意字面量联合的展示顺序一旦__len__的返回类型中包含非整数成员如foo或str该类型便不满足Sized协议对__len__必须返回 int 的要求len()调用触发invalid-argument-type诊断错误消息明确指出“ExpectedSized, foundOneOrFoo”同时揭示结果回退为int。纯布尔返回值的字面量布尔用例from typing import Literal class LiteralTrue: def __len__(self) - Literal[True]: return True class LiteralFalse: def __len__(self) - Literal[False]: return False reveal_type(len(LiteralTrue())) # revealed: Literal[1] reveal_type(len(LiteralFalse())) # revealed: Literal[0]Literal[True]→Literal[1]、Literal[False]→Literal[0]印证了“布尔隐式转 int”的规则。负整数当前回退为int诊断尚未实现from typing import Literal class Negative: def __len__(self) - Literal[-1]: return -1 # TODO: Emit a diagnostic reveal_type(len(Negative())) # revealed: int__len__返回Literal[-1]时len()揭示为int而非Literal[-1]因为non_negative_int_literal仅保留非负整数字面量负值直接落入_ None分支。测试注释# TODO: Emit a diagnostic与源码中的// TODO: Emit diagnostic for non-integers and negative integerstypes.rs相互印证目前对负数/非整数返回值的诊断尚未实现属于已知的待办项。错误的__len__签名与缺失__len__第二个可选参数合法from typing import Literal class SecondOptionalArgument: def __len__(self, v: int 0) - Literal[0]: return 0 # this is fine: the call succeeds at runtime since the second argument is optional reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]__len__允许带有默认值的额外参数——len(x)在运行时只传入x一个位置参数多余的参数由默认值补足调用可以成功因此len(SecondOptionalArgument())揭示为Literal[0]。第二个必需参数不满足Sized协议class SecondRequiredArgument: def __len__(self, v: int) - Literal[1]: return 1 # error: [invalid-argument-type] Argument to function len is incorrect: Expected Sized, found SecondRequiredArgument reveal_type(len(SecondRequiredArgument())) # revealed: int一旦__len__含必需的额外参数len()调用将无法匹配该签名调用参数个数不足该对象不被视为Sized产生invalid-argument-type诊断结果回退为int。完全没有__len__class NoDunderLen: ... # error: [invalid-argument-type] reveal_type(len(NoDunderLen())) # revealed: int没有任何__len__方法的类实例直接不满足Sized协议len()调用报invalid-argument-type错误揭示结果为int。实现原理Type::len与KnownFunction::Len绑定把上述测试规则与源码对应起来len()的完整推断路径如下绑定内置函数类型检查器在调用len时在 bind.rs 中识别出KnownFunction::Len取出第一个参数的类型并调用其len(db, env)方法若能得到比int更精确的类型就覆盖内置函数的返回类型。字面量短路Type::lentypes.rs首先检查类型是否为字符串/字节字面量字符串走StringLiteralType::python_len按 Unicode 字符计数字节串走BytesLiteralType::python_len按字节计数直接返回对应的int字面量类型。这就是len(no\rmal) → Literal[6]的出处。定长元组等其它精确长度场景则由TupleLength::Fixed等结构支撑tuple.rs。调用__len__dunder对非字面量类型Type::len通过try_call_dunder(db, env, __len__, CallArguments::none(), ...)以零参数调用该类型的__len__方法types.rs。这里CallArguments::none()正是“len()只传一个 self 位置参数”这一运行时语义的类型级建模——所以带必需第二参数的__len__无法绑定触发invalid-argument-type。错误回退策略当CallDunderError::MethodNotAvailable无__len__方法发生时返回None由调用方回退到 typeshed 中len()的声明返回类型int源码注释明确说明这一点。诊断消息Expected \Sized, found ...中的Sized对应 typing 协议typing.Sized要求len返回 int该错误码invalid-argument-type的文档位于 [types/diagnostic.rs](https://link.gitcode.com/i/23a2df0b5862546209bfb5b089d8126d) 引用的resources/lint_docs/invalid-argument-type.md。结果规范化最后经non_negative_int_literal处理——非负整数字面量保留、布尔转 0/1、联合递归映射、负值与其它类型回退types.rs。小结围绕 len.md 这份 mdtest 规范可以提炼出 Ruff ty 类型检查器对len()的完整规则表参数类型推断结果依据字符串 / 字节字面量含 raw、拼接、多行Literal[精确长度]按字符 / 按字节python_lenliteral.rs定长元组含子类、星号解包Literal[元素个数]TupleLength::Fixedtuple.rs变长元组tuple[T, ...]及其子类int长度未知列表、集合、字典、frozenset任意内容int运行时可变不做字面量推断__len__返回非负整数字面量 / 布尔对应Literal布尔转 0/1non_negative_int_literal__len__返回负数、非整数、含非法联合intinvalid-argument-type负数诊断为 TODOType::len无__len__或签名不兼容intinvalid-argument-typetry_call_dunder失败这些规则既是文档也是测试更是可复现的工程事实读者可以结合 Type::len 实现、KnownFunction::Len 绑定 与 mdtest 运行器 进一步验证也可以在编写依赖len()精确类型的类型级代码如重载__len__返回Literal的自定义类时直接参照上述行为约定。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表