ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Ruff ty 类型推断:枚举(Enum)成员相等性与不相等性比较的静态推导原理与实战

Ruff ty 类型推断:枚举(Enum)成员相等性与不相等性比较的静态推导原理与实战 Ruff ty 类型推断枚举Enum成员相等性与不相等性比较的静态推导原理与实战【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读在 Python 类型检查器中Enum成员的、!、is、is not运算结果往往是静态可知的Answer.NO Answer.NO恒为真而Choice.FIRST Choice.THIRD恒为假。本文以 Ruff 仓库中 ty 类型检查器的 mdtest 比较测试套件 enums.md 为骨架结合 equality.rs 与 equality/enums.rs 的源码实现系统讲解枚举比较结果的静态推导规则、跨类标量枚举的按值比较语义以及自定义__eq__/__ne__时的回退策略帮助读者理解 Ruff ty 如何在类型层面精确知道枚举比较的结果并将该知识用于分支窄化与不可达代码检测。一、测试载体mdtest 与 reveal_type本文讨论的所有revealed:结果均来自 Ruff 仓库中的 mdtestMarkdown 测试体系。mdtest 允许在 Markdown 文件的 Python 代码块中内嵌# revealed:、# error:等断言注释由测试框架解析、执行并核对输出见 crates/mdtest/src/lib.rs。reveal_type(...)断言用于检查类型检查器对某个表达式推断出的类型例如reveal_type(Answer.NO Answer.NO) # revealed: Literal[True]即要求 ty 将该表达式的结果类型精确推导为Literal[True]而非宽泛的bool。这正是静态已知比较结果的最直观体现。相关测试文件位于 crates/ty_python_semantic/resources/mdtest/comparison/其中 enums.md 专门覆盖枚举的相等性比较同目录下还有identity.md、strings.md、tuples.md、unions.md等配套主题。二、同一枚举内单例成员的比较恒真与恒假2.1 完整测试用例enums.md 的第一个用例是同一枚举Answer内部两个单例成员的四种比较运算from enum import Enum class Answer(Enum): NO 0 YES 1 reveal_type(Answer.NO Answer.NO) # revealed: Literal[True] reveal_type(Answer.NO Answer.YES) # revealed: Literal[False] reveal_type(Answer.NO ! Answer.NO) # revealed: Literal[False] reveal_type(Answer.NO ! Answer.YES) # revealed: Literal[True] reveal_type(Answer.NO is Answer.NO) # revealed: Literal[True] reveal_type(Answer.NO is Answer.YES) # revealed: Literal[False] reveal_type(Answer.NO is not Answer.NO) # revealed: Literal[False] reveal_type(Answer.NO is not Answer.YES) # revealed: Literal[True]对于默认object 语义枚举枚举成员之间按对象身份比较同一个成员对象与自己比较恒等与不同成员比较必然不等。因此同一成员与is恒真!与is not恒假不同成员与is恒假!与is not恒真。2.2 源码中的实现路径该结果由枚举字面量约束捷径产生。在 equality.rs 的enum_literal_constraint中当被比较的右操作数是枚举成员字面量LiteralValueTypeKind::Enum且左操作数的所有可能取值都属于同一个枚举域is_same_enum_domain见同文件 L1015-L1075时ty 会直接产出排除其他成员的窄化约束def f(answer: Answer): if answer ! Answer.NO: reveal_type(answer) # Literal[Answer.YES] else: reveal_type(answer) # Literal[Answer.NO]这是enum_literal_constraint的典型效果answer ! Answer.NO把answer从整个Answer域窄化到Literal[Answer.YES]。值得注意的是源码注释明确说明该捷径在枚举定义或继承了自定义__eq__/__ne__方法时会被禁用因为自定义方法可能改变成员间的相等性见 equality.rs 的注释。这对应后文第五节讨论的回退策略。此外is/is not的结果推导依托has_known_identity_comparison_semantics当两侧都具备已知的对象身份比较语义时ty 直接以两侧类型是否相等left right作为比较结果equality.rs。三、跨成员域的投影比较不展开枚举也能判断不等3.1 完整测试用例enums.md 的第二个用例说明即使两个操作数都不是单例只要两个枚举域不可能包含相等的值结果仍然可判from enum import Enum from typing import Literal class Choice(str, Enum): FIRST first SECOND second THIRD third FOURTH fourth def compare( left: Literal[Choice.FIRST, Choice.SECOND], right: Literal[Choice.THIRD, Choice.FOURTH], ): reveal_type(left right) # revealed: Literal[False] reveal_type(left ! right) # revealed: Literal[True]left只可能是Choice.FIRST或Choice.SECONDright只可能是Choice.THIRD或Choice.FOURTH两个成员集合没有任何交集因此left right对任意取值组合恒为假。3.2 源码实现SameEnumComparison 与 ProjectedEnumComparisonenums.md 开篇即点明设计动机Equality result inference uses the same runtime semantics as equality narrowing.——相等性结果推断与相等性窄化共享同一套运行时语义模型。在 equality/enums.rs 中枚举比较的入口是evaluate_enum_comparisonL25-L46它依次尝试两条路径evaluate_enum_domainsL52-L70把两侧都提取为枚举值域集合EnumDomainSet后若两侧都是同一枚举类的单例 → 交给SameEnumComparisonL270-L358逐对判断否则 → 交给ProjectedEnumComparisonL896-L963做键投影比较。PartitionedEnumComparisonL95-L264当一侧是枚举 其他值的联合如Left | None时枚举成员归并在一起比较一次其他值走通用比较路径。关键在于ProjectedEnumComparison不逐对展开所有成员组合而是把每个枚举类投影为一组比较键EnumKeyProjection见 L1012-L1041再判断两个投影键集合是否可能重叠may_overlap键集合完全不相交→ 相等性恒假Truthiness::AlwaysFalse不等式恒真两侧都只有一个键且键相同→ 相等性恒真否则→ 结果不确定Ambiguous但若正处于要求相等的条件分支中仍可产出窄化约束CanNarrow。这一设计避免了构造并逐对比较所有成员并集的开销是文件头注释 Compare enum values without checking every pair of membersenums.rs L20的直接体现。3.3 比较键的分类对象键与标量键比较键的类型定义在 enums.rs L965-L983enum EnumComparisonKeydb { Object(EnumClassLiteraldb, db Name), Scalar(LiteralValueTypeKinddb), }Object键默认枚举以枚举类 成员名作为身份标识Scalar键混合枚举如str混合枚举以其底层标量字面量作为比较键。enum_comparison_keyL1211-L1225负责把成员的底层值规约成键例如 Int 语义下bool值会被规约为0/1的整数字面量str/bytes语义下直接取字符串/字节字面量从而让键投影能够跨枚举类统一比较。四、跨类标量枚举按底层值比较4.1 完整测试用例enums.md 的第三个用例展示了不同枚举类之间的标量比较from enum import Enum class X(str, Enum): A a B b class Y(str, Enum): A a B b reveal_type(X.A Y.A) # revealed: Literal[True] reveal_type(X.A ! Y.A) # revealed: Literal[False] reveal_type(X.A Y.B) # revealed: Literal[False]尽管X与Y是不同类但二者都是str混合枚举成员底层值同为字符串。Python 运行时中X.A Y.A会委托给字符串的相等性语义因此结果为真X.A Y.B因为底层字符串不同而为假。4.2 源码依据跨类投影如何工作上述行为由ProjectedEnumComparison的键投影机制自然覆盖X.A与Y.A虽然enum_class不同但它们的Scalar键都是字符串字面量a投影键集合存在交集且两侧都是单键 → 相等性恒真enums.rs L918-L931 的truthiness分支。反之X.A Y.B的标量键分别是a与b投影键不相交 → 恒假。这里还隐含了一个与普通字面量比较一致的行为Literal[1]类型的对象与IntEnum成员Foo.X 1比较时恒为真尽管二者静态类型不相交。这正是 equality.rs L34-L45 中ComparisonResult::AlwaysTrue注释所强调的——AlwaysTrue/AlwaysFalse只描述运行时比较结果不表示两侧存在子类型或可赋值关系。相关键域的建模还体现在EnumComparisonKeyDomainenums.rs L985-L1009上Object、Int、Str、Bytes、Tuple、Dict六个域分别对应不同的已知比较语义KnownComparisonSemantics未知域如开放枚举的未声明运行成员则保守地按可能与已知域重叠处理unknown_overlaps_knownL1007-L1009。五、自定义相等性语义回退到 dunder 方法5.1 完整测试用例enums.md 的第四个用例说明当枚举自定义了__eq__/__ne__时结果推断不再使用内置语义而是回退到对应的 dunder 方法返回值from enum import Enum class EqResult: ... class NeResult: ... class CustomEquality(Enum): MEMBER 1 def __eq__(self, other: object) - EqResult: # error: [invalid-method-override] return EqResult() def __ne__(self, other: object) - NeResult: # error: [invalid-method-override] return NeResult() reveal_type(CustomEquality.MEMBER CustomEquality.MEMBER) # revealed: EqResult reveal_type(CustomEquality.MEMBER ! CustomEquality.MEMBER) # revealed: NeResult两个细节值得注意结果类型直接取自方法返回类型的结果类型是__eq__的返回类型EqResult!的结果类型是__ne__的返回类型NeResult——不再承诺Literal[True]/Literal[False]方法覆写会被诊断为[invalid-method-override]__eq__的标准签名要求返回bool返回EqResult属于不合法覆写ty 会同时报告该错误。5.2 源码依据为何必须回退源码在多处对自定义相等性保持保守enum_literal_constraint在枚举存在自定义__eq__/__ne__时整体禁用equality.rs L977-L1000finite_alternatives明确不展开带自定义比较方法的枚举类因为其成员可能等于枚举域之外的值equality.rs L1358-L1361在 equality.rs 模块头注释中写着该模块围绕已知的 Python 运行时语义评估比较产出分支约束与确定真值同时对自定义比较方法保持保守L1-L5。当内置语义不可用时ty 会退回到调用丰富比较 dunder 的通用路径如Type::try_call_rich_comparison_dunder见 equality.rs L321-L338把 dunder 的返回类型作为比较结果类型。5.3 语义与实现边界的总结结合整个 equality/enums.rs 与 equality.rs枚举比较的静态推导遵循以下优先级同一枚举、已知内置语义object/int/str/bytes用成员名或底层标量值构成的比较键精确判断恒真/恒假并支持分支窄化跨枚举类的标量比较按底层标量值跨类投影比较第四节用例开放枚举可含未声明运行时成员整体域被视为非封闭is_closed为假保守返回不确定或仅窄化自定义__eq__/__ne__放弃内置捷径回退到 dunder 返回类型第五节用例。对结果的处理统一收敛到ComparisonResult四种形态AlwaysTrue、AlwaysFalse、CanNarrow(narrowed)、Ambiguousequality.rs L30-L62。CanNarrow进一步被constraintL98-L109转换为具体分支Positive/Negative下的窄化约束从而驱动if answer ! Answer.NO这类条件分支的类型收窄与不可达代码检测。六、如何运行与验证这些测试这些枚举比较断言是 ty 类型检查器的回归测试可通过仓库的 mdtest 机制运行。相关环境变量定义于 crates/mdtest/src/lib.rsMDTEST_TEST_FILTER按名称过滤要运行的测试MDTEST_UPDATE_SNAPSHOTS1自动更新内联 snapshotMDTEST_GITHUB_ANNOTATIONS_FORMAT切换为 GitHub Actions 注释输出。针对enums.md所在测试套件归属于ty_python_semanticcrate可以执行# 运行 ty_python_semantic 的 mdtest 套件 cargo test -p ty_python_semantic --test mdtest # 仅运行名称包含 enums 的用例 MDTEST_TEST_FILTERenums cargo test -p ty_python_semantic --test mdtest若某个# revealed:断言与类型检查器实际输出不一致mdtest 会报告失败并提示用MDTEST_TEST_FILTER单独重跑该用例见 crates/mdtest/src/lib.rs L131-L147。这也意味着文中所列所有revealed:结果均由仓库测试套件强制约束是可直接验证的实现事实。结语从 enums.md 的四组用例可以看到Ruff ty 对枚举比较的静态推导是一套层次分明的策略单例成员直接判恒真/恒假跨成员域用比较键投影避免展开并集跨类标量枚举按底层值比较遇到自定义相等性则保守回退到 dunder 方法。这些能力全部建立在 equality.rs 与 equality/enums.rs 的ComparisonResult、SameEnumComparison、ProjectedEnumComparison、EnumKeyProjection等核心组件之上并最终服务于分支窄化与不可达代码分析。对于需要在代码中精确使用枚举比较语义的开发者而言理解哪些比较结果静态可知、哪些必须保守回退是写出可被类型检查器充分利用的枚举代码的关键。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表