
深入解析 Ruff ty 类型检查器的布尔推断and/or 表达式、真值分析与__bool__协议【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文以 Ruff 仓库内置类型检查器ty位于crates/ty_*的 Markdown 测试文档 expression/boolean.md 为核心系统讲解 ty 如何推断 Python 布尔上下文中的表达式类型包括and/or短路求值的类型传播、bool()调用对真值truthy/假值falsy/歧义ambiguous三类值的判定以及unsupported-bool-conversion诊断背后的__bool__协议分析。读完本文你将掌握 ty 类型检查器中布尔推断的完整规则体系并能通过仓库内的 mdtest 测试框架验证这些行为。一、测试载体mdtest 与reveal_type断言boolean.md是 ty 类型检查器 Markdown 测试套件mdtest中的一份用例文件。mdtest 以 Markdown 代码块为单位组织测试每个测试通过特殊注释表达断言reveal_type(expr) # revealed: Type要求类型检查器将expr推断为Type# error: [code] message要求对应代码位置产生指定的 lint 诊断。测试运行器位于 crates/ty_python_semantic/mdtest.py它负责收集resources/mdtest目录下的 Markdown 文件并交由cargo test --package ty_python_semantic --testmdtest执行Markdown 文件的解析与断言匹配逻辑则在 crates/mdtest/src 的parser.rs、matcher.rs、assertion.rs中实现。本文档属于expression/目录聚焦**布尔上下文boolean context**中的类型推断。理解其语义的核心是 ty 底层的三值真值模型。二、真值三值模型Truthiness枚举ty 并不把对象在if中的表现当作简单的bool二值问题而是引入了一个三值枚举Truthiness定义于 crates/ty_python_core/src/lib.rs#[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum Truthiness { /// For an object x, bool(x) will always return True AlwaysTrue, /// For an object x, bool(x) will always return False AlwaysFalse, /// For an object x, bool(x) could return either True or False Ambiguous, }AlwaysTruebool(x)恒为True例如非空字符串、非零整数、函数对象AlwaysFalsebool(x)恒为False例如0、、NoneAmbiguous取决于运行期值可能是True也可能是False例如[]、{}、普通bool。该枚举还提供了is_always_true、is_always_false、may_be_true、negate、and等组合方法and/or表达式的真值推断正是基于这些方法在三值域上做短路运算。理解了这一点下面文档中的# revealed:结果就有了统一解释框架类型检查器对表达式的类型进行静态分析只有当它能证明所有取值都落在同一真值时才会给出Literal[True]/Literal[False]否则给出bool即 Ambiguous。三、or表达式的类型推断Python 的or遵循短路求值返回第一个为真的操作数若全部为假则返回最后一个操作数。ty 据此做操作数级的类型推断而不是简单的bool合并。文档中的OR用例def _(foo: str): reveal_type(True or False) # revealed: Literal[True] reveal_type(x or y or z) # revealed: Literal[x] reveal_type( or y or z) # revealed: Literal[y] reveal_type(False or z) # revealed: Literal[z] reveal_type(False or True) # revealed: Literal[True] reveal_type(False or False) # revealed: Literal[False] reveal_type(foo or False) # revealed: (str ~AlwaysFalsy) | Literal[False] reveal_type(foo or True) # revealed: (str ~AlwaysTruthy) | Literal[True]逐条解读True or False首个操作数恒为真直接短路结果为Literal[True]x or y or zx恒真结果为字面量类型Literal[x] or y or z恒假被跳过结果为Literal[y]False or z结果为Literal[z]foo or False与foo or Truefoo: str无法静态确定是否为空串因此结果是一个集合论类型——(str ~AlwaysFalsy) | Literal[False]。含义是当foo非空~AlwaysFalsy表示不会恒为假的str部分时结果为str当foo为空串时结果为Literal[False]。注意foo or True中空串分支的结果是Literal[True]因此补集写作~AlwaysTruthy不会恒为真的str部分即空串这正是集合论类型系统见 types/set_theoretic的表达能力所在。四、and表达式的类型推断and与or对称返回第一个为假的操作数若全部为真则返回最后一个操作数。文档中的AND用例def _(foo: str): reveal_type(True and False) # revealed: Literal[False] reveal_type(False and True) # revealed: Literal[False] reveal_type(foo and False) # revealed: (str ~AlwaysTruthy) | Literal[False] reveal_type(foo and True) # revealed: (str ~AlwaysTruthy) | Literal[True] reveal_type(x and y and z) # revealed: Literal[z] reveal_type(x and y and ) # revealed: Literal[] reveal_type( and y) # revealed: Literal[]要点True and False→Literal[False]首个恒真继续求值返回第二个操作数x and y and z全部恒真返回最后一个操作数Literal[z]x and y and →Literal[]最后一个操作数本身即结果foo and False与foo and True当foo为假空串即str ~AlwaysTruthy部分时直接返回foo否则返回第二个操作数Literal[False]/Literal[True]。五、复合表达式and/or混合与优先级文档Complex一节验证了and优先级高于or时的类型传播。Python 中and比or绑定更紧因此a and b or c等价于(a and b) or creveal_type(x and y or z) # revealed: Literal[y] reveal_type(x or y and z) # revealed: Literal[x] reveal_type( and y or z) # revealed: Literal[z] reveal_type( or y and z) # revealed: Literal[z] reveal_type(x and y or ) # revealed: Literal[y] reveal_type(x or y and ) # revealed: Literal[x]逐项推导x and y or z(x and y) or zy or zLiteral[y]x or y and zx or (y and z) 首项恒真短路 →Literal[x] and y or z( and y) or z or zLiteral[z] or y and z or (y and z) or zLiteral[z]x and y or (x and y) or y or Literal[y]x or y and x or (y and )Literal[x]短路。这些用例说明 ty 的推断结果与 Python 运行期语义完全一致——它并不只是机械地按优先级合并类型而是逐层做短路传播。六、简单函数调用与bool()的推断6.1 控制流合并后的bool恢复文档Simple function calls to bool一节展示了一个典型的控制流合并场景def _(flag: bool): if flag: x True else: x False reveal_type(x) # revealed: bool两个分支分别给x赋True/False合并后类型为Literal[True] | Literal[False]等价于普通bool因此reveal_type显示bool而非两个字面量的并集。这说明 ty 在合并bool字面量分支时做了归一化处理。6.2bool()函数解析为内置行为文档bool() function的Evaluates to builtin一节区分了调用真正的内置bool与调用普通返回bool的函数a.pyredefined_builtin_bool: type[bool] bool def my_bool(x) - bool: return Truefrom a import redefined_builtin_bool, my_bool reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False] reveal_type(my_bool(0)) # revealed: bool关键差异redefined_builtin_bool的类型是type[bool]ty 能识别出它实际上就是内置bool类因此会走内置的真值分析——bool(0)恒为Literal[False]而my_bool只是一个普通函数其返回类型标注为boolty 不做内部实现分析直接给出bool。这体现了 ty 对KnownClass::Bool等已知类的特殊处理见下文源码分析。七、真值判定细则truthy / falsy / ambiguousbool(x)的静态结果取决于x的类型结构。文档用三个小节系统给出了判定规则。7.1 Truthy values恒为真以下用例在python-version 3.11环境下全部得到Literal[True][environment] python-version 3.11import enum from typing import Literal, final reveal_type(bool(1)) # revealed: Literal[True] reveal_type(bool((0,))) # revealed: Literal[True] reveal_type(bool(NON EMPTY)) # revealed: Literal[True] reveal_type(bool(True)) # revealed: Literal[True] def foo(): ... reveal_type(bool(foo)) # revealed: Literal[True] class SingleElementTupleSubclass(tuple[int]): ... reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True] # Unknown length, but we know the length is guaranteed to be 2 class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ... reveal_type(bool(MixedTupleSubclass((1, bfoo)))) # revealed: Literal[True] # Unknown length with an overridden __bool__: class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]): def __bool__(self) - Literal[True]: return True reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True] # Same again but for a subclass of a fixed-length tuple: class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]): # TODO: we should reject this override as a Liskov violation: def __bool__(self) - Literal[True]: return True reveal_type(bool(EmptyTupleSubclassWithDunderBoolOverride(()))) # revealed: Literal[True] reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) - Literal[True] # revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() - Literal[True] reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__) final class FinalClassOverridingLenAndNotBool: def __len__(self) - Literal[42]: return 42 reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[True] final class FinalClassWithNoLenOrBool: ... reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True] class EnumWithMembers(enum.Enum): A 1 B 2 reveal_type(bool(EnumWithMembers.A)) # revealed: Literal[True] def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool | Literal[EnumWithMembers.A]): reveal_type(bool(x)) # revealed: Literal[True]这些用例揭示了多条判定规则字面量/基础类型非零整数1、非空元组(0,)、非空字符串、True字面量恒为真函数对象foo是函数函数对象恒为真定长元组tuple[int]的子类实例至少包含 1 个元素恒为真变参元组tuple[int, *tuple[str, ...], bytes]即使长度未知也保证至少 2 个元素恒为真__bool__优先级元组子类若重写了__bool__且返回类型为Literal[True]即使它是空元组子类也按__bool__结果判定__bool__优先于元组长度推断final类 __len__final类没有__bool__但有返回Literal[42]的__len__判定为真final类且无任何 dunder既没有__bool__也没有__len__的final类实例恒为真枚举成员enum.Enum的成员实例恒为真全真联合当联合类型的每个分支都恒为真时整个联合恒为真。7.2 Falsy values恒为假import enum from typing import final, Literal reveal_type(bool(0)) # revealed: Literal[False] reveal_type(bool(())) # revealed: Literal[False] reveal_type(bool(None)) # revealed: Literal[False] reveal_type(bool()) # revealed: Literal[False] reveal_type(bool(False)) # revealed: Literal[False] reveal_type(bool()) # revealed: Literal[False] class EmptyTupleSubclass(tuple[()]): ... reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False] final class FinalClassOverridingLenAndNotBool: def __len__(self) - Literal[0]: return 0 reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False] class EnumWithMembersOverridingBool(enum.Enum): A 1 B 2 def __bool__(self) - Literal[False]: return False reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: Literal[False] def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool | Literal[EnumWithMembersOverridingBool.A]): reveal_type(bool(x)) # revealed: Literal[False]与上一节完全对称的假值规则零值整数、空元组、None、空串、False、无参bool()均恒为假空元组子类tuple[()]恒为假final类重写__len__返回Literal[0]恒为假枚举成员重写__bool__返回Literal[False]恒为假全假联合恒为假。7.3 Ambiguous values无法静态判定import enum from typing import Literal reveal_type(bool([])) # revealed: bool reveal_type(bool({})) # revealed: bool reveal_type(bool(set())) # revealed: bool class VariadicTupleSubclass(tuple[int, ...]): ... def f(x: tuple[int, ...], y: VariadicTupleSubclass): reveal_type(bool(x)) # revealed: bool class NonFinalOverridingLenAndNotBool: def __len__(self) - Literal[42]: return 42 # We cannot consider __len__ for a non-final type, # because a subclass might override __bool__, # and __bool__ takes precedence over __len__ reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool class EnumWithMembersOverridingBool(enum.Enum): A 1 B 2 def __bool__(self) - bool: return False reveal_type(bool(EnumWithMembersOverridingBool.A)) # revealed: bool歧义判定的核心场景与设计动机可变长度容器[]、{}、set()的字面量类型虽然是空集合但list/dict/set实例的真值取决于长度ty 保守地给出bool长度未知的元组tuple[int, ...]及其子类长度不可知结果为bool非final类只重写__len__这是最值得注意的一条。文档注释明确解释了原因——非final类型不能依据__len__判定因为子类可能重写__bool__而__bool__的优先级高于__len__。也就是说NonFinalOverridingLenAndNotBool()的某个子类完全可能定义__bool__返回False因此静态上只能给出bool__bool__返回非字面量bool__bool__的返回类型是普通bool而非Literal[False]结果自然也是bool。八、底层原理try_bool_impl的判定逻辑上述规则在 crates/ty_python_semantic/src/types/bool.rs 的Type::try_bool_impl中实现。它按类型结构分派字面量直接映射Literal[True]/Literal[False]直接映射整数字面量按! 0映射字符串/字节字面量按!is_empty()映射__bool__调用优先尝试调用__bool__校验返回类型是否可赋值给boolis_assignable_to(KnownClass::Bool)否则产生BoolError::IncorrectReturnType若__bool__可能缺失PossiblyUnbound则保守返回Ambiguous元组特判对没有__bool__的元组实例直接使用tuple_spec.truthiness()基于元组长度规格final类特判只有final类才回退考虑__len__返回类型须可赋值给SupportsIndex若__len__也不存在则恒为AlwaysTrue。这与文档中非final类不依据__len__的注释完全对应函数与特殊类型is_function_like的可调用对象、绑定方法、模块字面量等恒为AlwaysTrueNever、Dynamic等为Ambiguous联合类型逐个计算每个分支的真值若分支间真值不一致则合并为Ambiguous分支出错时聚合错误详见下一节循环防护通过TryBoolVisitorCycleDetector防止递归类型导致死循环。值得注意的实现细节try_bool_impl有一个allow_short_circuit参数——当只关心能否判定而不关心错误收集时如Type::bool用于静态分支分析一旦联合的某个分支为Ambiguous即可提前返回源码注释说明这在基准测试中带来了 1%–2% 的性能提升而类型检查场景使用Type::try_boolallow_short_circuit false以便完整收集诊断错误。九、错误诊断unsupported-bool-conversion当对象的__bool__实现不正确时ty 会报告 lintunsupported-bool-conversion。该 lint 声明于 crates/ty_python_semantic/src/types/diagnostic.rs描述为detects boolean conversion where the object incorrectly implements__bool__默认级别为Error。BoolError定义于 types/bool.rs区分了五种错误形态NotCallable__bool__不可调用、IncorrectArguments__bool__参数不正确、IncorrectReturnType返回类型不可赋值给bool、Union联合中某个变体实现不正确、Other其他情况。诊断信息还会附带__bool__方法定义位置的注解。文档给出了四类典型触发场景。9.1__bool__返回NoReturnfrom typing import NoReturn class NotBoolable: def __bool__(self) - NoReturn: raise NotImplementedError(This object cant be converted to a boolean) # TODO: This should emit an error that NotBoolable cant be converted to a bool but it currently doesnt # because Never is assignable to bool. This probably requires dead code analysis to fix. if NotBoolable(): pass这是文档明确标注的已知局限__bool__返回NoReturn即Never时由于Never可赋值给bool当前版本尚不会报错需要死代码分析才能正确诊断。该 TODO 也印证了try_bool_impl中return_type.is_assignable_to(bool)检查存在Never特例。9.2__bool__不可调用class NotBoolable: __bool__: None None # error: [unsupported-bool-conversion] Boolean conversion is not supported for type NotBoolable if NotBoolable(): pass__bool__被赋值为None不是可调用对象对应BoolError::NotCallable在if条件处报告unsupported-bool-conversion。9.3 不可布尔化的联合def test(cond: bool): class NotBoolable: __bool__: int | None None if cond else 3 # error: [unsupported-bool-conversion] Boolean conversion is not supported for type NotBoolable if NotBoolable(): pass__bool__的类型是int | Noneint分支不可调用、None分支不可调用整体不可布尔化对应BoolError::NotCallable。9.4 联合中部分变体实现错误from typing import Literal class NotBoolable: __bool__: None None def test(a: Literal[10] | NotBoolable): # error: [unsupported-bool-conversion] Boolean conversion is not supported for type Literal[10] | NotBoolable if a: pass联合Literal[10] | NotBoolable中Literal[10]本身恒真但NotBoolable的__bool__不可调用。对应源码中try_union的逻辑遍历分支收集错误若并非所有分支都不可调用则产生BoolError::Union其诊断消息会具体指出是哪一个变体NotBoolable未正确实现__bool__。十、总结boolean.md这份 mdtest 文档完整刻画了 ty 类型检查器的布尔推断能力可以归纳为三层模型表达式层and/or按短路语义在操作数间传播字面量类型并通过~AlwaysTruthy/~AlwaysFalsy集合论标记表达条件分支的结果类型类型层bool()基于Truthiness三值模型对类型结构做静态真值判定——字面量直接映射、元组按长度规格、final类回退__len__、函数对象恒真其余保守归为Ambiguous协议层__bool__校验__bool__的可调用性、参数与返回类型并将错误聚合为unsupported-bool-conversion诊断同时保留对__bool__返回NoReturn、非final类重写__len__等边缘场景的已知限制说明。对类型检查器使用者而言这些规则直接决定了if条件、and/or表达式的告警质量与类型收窄精度对类型检查器开发者而言这份文档与 types/bool.rs 源码互为印证是理解 Ruff 系ty项目布尔语义的最佳入口。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考