拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ruff 类型检查器(ty)如何为 `@functools.total_ordering` 合成比较方法:mdtest 行为规范与源码实现解析

Ruff 类型检查器ty如何为functools.total_ordering合成比较方法mdtest 行为规范与源码实现解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文以仓库内类型检查器ty的 mdtest 行为测试文档 crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md 为骨架系统梳理functools.total_ordering装饰器在静态类型推断中的完整语义比较方法合成规则、root 方法优先级、签名推导、重载保留、继承与动态类type()支持并结合ty_python_semanticcrate 的源码逐条印证实现原理。读完你不仅能理解该测试的全部断言行为还能定位到合成逻辑、诊断上报与装饰器识别在仓库中的具体实现位置。背景这份 mdtest 文档在仓库中的角色total_ordering.md是 Ruff 仓库中类型检查器ty的mdtest 行为规范文档存放于 crates/ty_python_semantic/resources/mdtest/decorators/ 目录。这类文档以 Markdown 内嵌 Python 代码的方式通过reveal_type(...)期望推断出的类型与# error: [...]期望诊断错误码两条机制把类型检查器的行为固化为可执行断言。文档的核心主题是当类使用functools.total_ordering装饰器时ty类型检查器如何像 Python 运行时一样基于用户定义的单个比较方法如__lt__自动合成出其余比较方法__le__、__gt__、__ge__的类型签名并给出正确的推断结果或诊断信息。total_ordering本身的行为很直观它要求类至少定义一个排序方法运行时据此生成其余方法。但静态类型推断要还原这套逻辑需要解决一系列边界问题方法签名怎么继承、重载怎么保留、继承来的方法算不算数、动态命名空间怎么办、缺失方法时报什么错——这正是本文档逐节覆盖的内容。基本用法从单个__lt__合成全套比较方法文档开头给出了最基础也最核心的语义当一个类定义了__eq__与__lt__时装饰器自动合成__le__、__gt__、__ge__from functools import total_ordering total_ordering class Student: def __init__(self, grade: int): self.grade grade def __eq__(self, other: object) - bool: if not isinstance(other, Student): return NotImplemented return self.grade other.grade def __lt__(self, other: Student) - bool: return self.grade other.grade s1 Student(85) s2 Student(90) # 用户自定义的比较方法工作正常 reveal_type(s1 s2) # revealed: bool reveal_type(s1 s2) # revealed: bool # 合成的比较方法可用 reveal_type(s1 s2) # revealed: bool reveal_type(s1 s2) # revealed: bool reveal_type(s1 s2) # revealed: bool这里有两个可验证的推断事实与返回用户声明的方法返回类型bool而、、即便在源码里不存在也被推断为bool——说明类型检查器确实看见了装饰器合成的方法。源码印证合成发生在own_synthesized_member从源码看合成逻辑位于 crates/ty_python_semantic/src/types/class/static_literal.rs 的own_synthesized_member约 L1646-L1700。该函数处理functools.total_ordering时满足以下条件才合成类带有total_ordering标记即被装饰器修饰请求的方法名属于__lt__、__le__、__gt__、__ge__之一该方法尚未在 MRO 中显式定义显式定义的不覆盖类在 MRO排除object中至少存在一个排序方法has_ordering_method_in_mro能取得 root 排序方法并 upcast 为可调用类型。满足条件后合成方法会克隆 root 方法的参数列表并把返回类型与bool取并集UnionType::from_two_elements原因在后文非 bool 返回类型一节详细说明。与类保持装饰器叠加元数据穿透实际代码中total_ordering常常与其他装饰器一起使用。文档断言当一个显式标注返回类型且保持类身份class-preserving的装饰器位于total_ordering之下时total_ordering的元数据依然生效from functools import total_ordering from typing import TypeVar T TypeVar(T, boundobject) def identity(cls: type[T]) - type[T]: return cls total_ordering identity class OrderedIdentity: def __eq__(self, other: object) - bool: return isinstance(other, OrderedIdentity) def __lt__(self, other: OrderedIdentity) - bool: return True left OrderedIdentity() right OrderedIdentity() reveal_type(left right) # revealed: bool reveal_type(left right) # revealed: bool即与依然被合成。源码印证装饰器识别在class.rstotal_ordering的标记是如何进入类字面量的在 crates/ty_python_semantic/src/types/infer/builder/class.rs约 L186-L192中类型检查器遍历装饰器列表通过function.is_known(db, KnownFunction::TotalOrdering)识别出functools.total_ordering随后把total_ordering true写入StaticClassLiteral。而StaticClassLiteral上的total_ordering字段static_literal.rs L96-L98注释为 Whether this class is decorated withfunctools.total_ordering是后续一切合成与校验的总开关。函数级已知标识KnownFunction::TotalOrdering的定义与判定见 crates/ty_python_semantic/src/types/function.rsL2321 枚举项、L2445 处要求module.is_functools()。签名推导从源排序方法继承参数类型当源排序方法的other参数接受更宽的类型比如object时合成方法的签名必须保持一致否则会出现合成的无法与int比较的误报。文档中的Comparable示例演示了这一点from functools import total_ordering total_ordering class Comparable: def __init__(self, value: int): self.value value def __eq__(self, other: object) - bool: if isinstance(other, Comparable): return self.value other.value if isinstance(other, int): return self.value other return NotImplemented def __lt__(self, other: object) - bool: if isinstance(other, Comparable): return self.value other.value if isinstance(other, int): return self.value other return NotImplemented a Comparable(10) b Comparable(20) reveal_type(a b) # revealed: bool reveal_type(a b) # revealed: bool # 因为 __lt__ 接受 object与 int 的比较也成立 reveal_type(a 15) # revealed: bool reveal_type(a 5) # revealed: bool这与源码实现完全吻合own_synthesized_member中Signature::new_generic直接复用root_method_ty的parameters()即参数签名一字不差地继承自 root 排序方法仅返回类型被改造为原返回类型 ∪ bool。Root 方法选择__lt____le____gt____ge__当类定义了多个签名不同的排序方法时装饰器需要一个根方法作为合成基准。文档明确给出优先级顺序__lt____le____gt____ge__并且已显式定义的方法不会被覆盖from functools import total_ordering total_ordering class MultiSig: def __init__(self, value: int): self.value value def __eq__(self, other: object) - bool: return True # __lt__ 接受 object最高优先级作为 root def __lt__(self, other: object) - bool: return True # __gt__ 只接受 MultiSig不被装饰器覆盖 def __gt__(self, other: MultiSig) - bool: return True a MultiSig(10) b MultiSig(20) # __le__ 与 __ge__ 使用 __lt__ 的签名接受 object reveal_type(a b) # revealed: bool reveal_type(a 15) # revealed: bool reveal_type(a b) # revealed: bool reveal_type(a 15) # revealed: bool # __gt__ 保留原始签名只接受 MultiSig reveal_type(a b) # revealed: bool a 15 # error: [unsupported-operator]注意最后一个断言的戏剧性a 15合法来自合成的__le__继承__lt__的宽签名而a 15报unsupported-operator错误因为__gt__是显式定义的保留窄签名。这验证了显式定义优先、合成只补缺的设计。源码印证total_ordering_root_method与 MRO 遍历total_ordering_root_methodstatic_literal.rs L309-L357正是优先级规则的实现内部定义const ORDERING_METHODS: [str; 4] [__lt__, __le__, __gt__, __ge__]按此顺序外层遍历对每个方法名再沿iter_mro遍历 MRO跳过object命中即返回。因此它天然满足两点优先级固定、无论方法定义在本地还是继承自父类均一视同仁。同时own_synthesized_member中不合成 MRO 已存在的方法的条件遍历 MRO 检查同名方法是否存在保证了显式定义不被覆盖。而has_own_ordering_methodL282-L288与has_own_comparison_methodsL290-L296分别提供是否定义了任意一个/全部四个排序方法的辅助判定。重载的排序方法保留全部 overload如果 root 排序方法使用了overload合成方法必须保留全部重载签名。文档用Flexible类验证from functools import total_ordering from typing import overload total_ordering class Flexible: def __init__(self, value: int): self.value value def __eq__(self, other: object) - bool: return True overload def __lt__(self, other: Flexible) - bool: ... overload def __lt__(self, other: int) - bool: ... def __lt__(self, other: Flexible | int) - bool: if isinstance(other, Flexible): return self.value other.value return self.value other a Flexible(10) b Flexible(20) # 合成的 __le__ 保留 __lt__ 的重载 reveal_type(a b) # revealed: bool reveal_type(a 15) # revealed: bool # 合成的 __ge__ 同样保留重载 reveal_type(a b) # revealed: bool reveal_type(a 15) # revealed: bool # 与不支持的类型比较仍应报错 a string # error: [unsupported-operator]源码印证CallableSignature::from_overloads源码中own_synthesized_member使用CallableSignature::from_overloads(callable.signatures(db).iter().map(...))逐个转换 root 方法的所有签名从而把重载集合整体搬到合成方法上static_literal.rs L1680-L1696。这也是a 15合法、a string报错的底层原因重载集合中只有Flexible与int两个分支。__gt__作为 root对称合成优先级并不总是从__lt__出发。当类只定义__eq__与__gt__时装饰器以__gt__为 root 合成__lt__、__le__、__ge__from functools import total_ordering total_ordering class Priority: def __init__(self, level: int): self.level level def __eq__(self, other: object) - bool: if not isinstance(other, Priority): return NotImplemented return self.level other.level def __gt__(self, other: Priority) - bool: return self.level other.level p1 Priority(1) p2 Priority(2) reveal_type(p1 p2) # revealed: bool用户定义 reveal_type(p1 p2) # revealed: bool用户定义 reveal_type(p1 p2) # revealed: bool合成 reveal_type(p1 p2) # revealed: bool合成 reveal_type(p1 p2) # revealed: bool合成继承场景一__eq__从object继承文档强调__eq__是可选的——它可以继承自object类只需定义一个排序方法即可触发合成from functools import total_ordering total_ordering class Score: def __init__(self, value: int): self.value value def __lt__(self, other: Score) - bool: return self.value other.value s1 Score(85) s2 Score(90) reveal_type(s1 s2) # revealed: bool继承自 object reveal_type(s1 s2) # revealed: bool reveal_type(s1 s2) # revealed: bool reveal_type(s1 s2) # revealed: bool继承场景二排序方法来自父类装饰器同样支持排序方法继承自父类的情形from functools import total_ordering class Base: def __lt__(self, other: Base) - bool: return True total_ordering class Child(Base): def __eq__(self, other: object) - bool: if not isinstance(other, Child): return NotImplemented return True c1 Child() c2 Child() # 即使 __lt__ 是继承来的合成方法依然生效 reveal_type(c1 c2) # revealed: bool reveal_type(c1 c2) # revealed: bool reveal_type(c1 c2) # revealed: bool继承场景三优先级不受本地/继承影响优先级规则的一个重要推论是无论方法定义在本地还是继承自父类__lt__的优先级都高于__gt__。文档给出了一个反直觉的例子——本地定义了__gt__但继承的__lt__才是 rootfrom functools import total_ordering from typing import Literal class Base: def __lt__(self, other: Base) - Literal[True]: return True total_ordering class Child(Base): # __gt__ 定义在本地但 __lt__继承优先 def __gt__(self, other: Child) - Literal[False]: return False c1 Child() c2 Child() reveal_type(c1 c2) # revealed: Literal[True] 继承自 Base reveal_type(c1 c2) # revealed: Literal[False] Child 本地定义 # __le__ 与 __ge__ 基于 __lt__ 合成即使 __gt__ 定义在类自身 reveal_type(c1 c2) # revealed: bool reveal_type(c1 c2) # revealed: bool这里用Literal[True]/Literal[False]精确区分了方法来源返回Literal[True]证明它来自Base.__lt__返回Literal[False]证明它是Child.__gt__原样保留。而合成的/返回普通bool——合成方法的返回类型是root 返回类型 ∪ boolLiteral[True] | bool化简为bool。显式定义的方法不被覆盖承接上一点文档用更窄的返回类型验证显式定义优先原则from functools import total_ordering from typing import Literal total_ordering class Temperature: def __init__(self, celsius: float): self.celsius celsius def __lt__(self, other: Temperature) - Literal[True]: return True def __gt__(self, other: Temperature) - Literal[True]: return True t1 Temperature(20.0) t2 Temperature(25.0) # 用户定义的方法保留其返回类型 reveal_type(t1 t2) # revealed: Literal[True] reveal_type(t1 t2) # revealed: Literal[True] # 合成方法的返回类型为 bool reveal_type(t1 t2) # revealed: bool reveal_type(t1 t2) # revealed: bool__le__、__ge__的返回类型是Literal[True] | bool并集化简后的bool而显式的__lt__、__gt__精确保持Literal[True]——两者形成鲜明对比正是own_synthesized_member中仅在 MRO 未定义该方法时才合成逻辑的直接体现。与dataclass组合使用total_ordering与dataclass可以安全叠加。dataclass 会合成__eq__用户再定义__lt__其余方法由total_ordering补齐from dataclasses import dataclass from functools import total_ordering total_ordering dataclass class Point: x: int y: int def __lt__(self, other: Point) - bool: return (self.x, self.y) (other.x, other.y) p1 Point(1, 2) p2 Point(3, 4) reveal_type(p1 p2) # revealed: booldataclass 合成的 __eq__ reveal_type(p1 p2) # revealed: bool用户定义 reveal_type(p1 p2) # revealed: booltotal_ordering 合成 reveal_type(p1 p2) # revealed: booltotal_ordering 合成 reveal_type(p1 p2) # revealed: booltotal_ordering 合成这与源码中own_synthesized_member的定位一致它是 dataclass、NamedTuple、total_ordering 等多条合成路径的公共入口函数注释 Returns the type of a synthesized dataclass member like__init__or__lt__, or a synthesized__new__method for aNamedTuple见 static_literal.rs L1644-L1653total_ordering 分支只是其中一条。缺失排序方法invalid-total-ordering诊断如果类带了total_ordering却连一个排序方法都没定义自身与父类都没有类型检查器会在装饰器所在位置上报invalid-total-ordering诊断且由于没有合成任何方法比较运算符本身也报unsupported-operatorfrom functools import total_ordering total_ordering # error: [invalid-total-ordering] class NoOrdering: def __eq__(self, other: object) - bool: return True n1 NoOrdering() n2 NoOrdering() n1 n2 # error: [unsupported-operator] n1 n2 # error: [unsupported-operator]这与 Python 运行时行为一致total_ordering要求至少一个排序方法否则抛出ValueError。源码印证诊断上报链路诊断上报发生在 crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs约 L643-L653当class.total_ordering(db)为真且!class.has_ordering_method_in_mro(db, None)时在decorator_list中定位到KnownFunction::TotalOrdering对应的装饰器节点调用report_invalid_total_ordering上报。诊断的规则文档位于 crates/ty_python_semantic/resources/lint_docs/invalid-total-ordering.md其中明确说明未定义排序方法时 Python 会在运行时抛出ValueError并给出修正示例补上__lt__。诊断注册report_invalid_total_ordering等导入见同一文件的 L39。规则汇总可查阅 crates/ty/docs/rules.md。没有装饰器时不合成对照实验不加total_ordering时只定义__lt__的类不会有__le__、__ge__。这里还揭示了一个 Python 语义细节——n1 n2之所以可用是因为 Python 会把反射reflected为n2 n1class NoDecorator: def __init__(self, value: int): self.value value def __eq__(self, other: object) - bool: if not isinstance(other, NoDecorator): return NotImplemented return self.value other.value def __lt__(self, other: NoDecorator) - bool: return self.value other.value n1 NoDecorator(1) n2 NoDecorator(2) reveal_type(n1 n2) # revealed: bool reveal_type(n1 n2) # revealed: bool # n1 n2 可用是因为 Python 反射为 n2 n1 reveal_type(n1 n2) # revealed: bool n1 n2 # error: [unsupported-operator] n1 n2 # error: [unsupported-operator]这从反面说明total_ordering分支的合成是有装饰器标记才触发的self.total_ordering(db)是own_synthesized_member的第一个条件反射式比较不在该合成机制的覆盖范围内。非 bool 返回类型合成方法的类型并集语义这是文档技术密度最高的部分。total_ordering生成的代码形如def __le__(self, other): return self other or self other因此合成方法的返回类型应该是root 方法的返回类型 ∪ bool。文档给出两种情形情形一root 返回intbool是int的子类型并集化简为intfrom functools import total_ordering total_ordering class IntReturn: def __init__(self, value: int): self.value value def __eq__(self, other: object) - bool: if not isinstance(other, IntReturn): return NotImplemented return self.value other.value def __lt__(self, other: IntReturn) - int: return self.value - other.value a IntReturn(10) b IntReturn(20) reveal_type(a b) # revealed: int用户定义 # int | bool 并集因 bool 是 int 子类型而化简为 int reveal_type(a b) # revealed: int reveal_type(a b) # revealed: int reveal_type(a b) # revealed: int情形二root 返回strbool不是str的子类型并集被保留from functools import total_ordering total_ordering class StrReturn: def __init__(self, value: str): self.value value def __eq__(self, other: object) - bool: if not isinstance(other, StrReturn): return NotImplemented return self.value other.value def __lt__(self, other: StrReturn) - str: return self.value a StrReturn(a) b StrReturn(b) reveal_type(a b) # revealed: str reveal_type(a b) # revealed: str | bool reveal_type(a b) # revealed: str | bool reveal_type(a b) # revealed: str | bool源码印证UnionType::from_two_elements实现位于own_synthesized_member的签名转换闭包中static_literal.rs L1679-L1695let return_ty UnionType::from_two_elements(db, env, signature.return_ty, bool_ty);把每个重载的返回类型与bool_ty取并集后重建签名。并集化简int | bool→int由类型系统的并集化简规则完成与文档注释的解释boolis a subtype ofintin Python一致。函数调用形式total_ordering(cls)装饰器语法total_ordering等价于total_ordering(cls)。函数调用形式执行同样的校验——无排序方法时报invalid-total-ordering有则返回原类类型from functools import total_ordering class NoOrderingMethod: def __eq__(self, other: object) - bool: return True # error: [invalid-total-ordering] InvalidOrderedClass total_ordering(NoOrderingMethod)from functools import total_ordering class HasOrderingMethod: def __eq__(self, other: object) - bool: return True def __lt__(self, other: HasOrderingMethod) - bool: return True # 无错误类定义了 __lt__ ValidOrderedClass total_ordering(HasOrderingMethod) reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]源码印证KnownFunction::TotalOrdering的调用检查函数调用形式的校验位于 crates/ty_python_semantic/src/types/function.rs约 L3030-L3050对KnownFunction::TotalOrdering取第一个参数并匹配Type::ClassLiteral/Type::GenericAlias随后调用class.has_ordering_method_in_mro(db)检查若缺失则调用report_invalid_total_ordering_call上报诊断。注意这里与装饰器形式共享同一个has_ordering_method_in_mro因此两类入口的校验口径完全一致。函数调用形式 type()动态类total_ordering也可以作用于用type()构造的类。只要动态类的命名空间里提供了排序方法就不会报错from functools import total_ordering def lt_impl(self, other) - bool: return True # 无错误函数式类在命名空间中定义了 __lt__ ValidFunctional total_ordering(type(ValidFunctional, (), {__lt__: lt_impl})) InvalidFunctionalBase type(InvalidFunctionalBase, (), {}) # error: [invalid-total-ordering] InvalidFunctional total_ordering(InvalidFunctionalBase)源码印证动态类字面量的成员查询total_ordering_root_method对ClassLiteral::Dynamic分支专门处理static_literal.rs L341-L348通过dynamic.own_class_member(db, name)在动态命名空间中查找排序方法因此type(..., {__lt__: lt_impl})能被正确识别。这解释了为什么ValidFunctional合法而InvalidFunctional报错。继承自函数式类当一个类继承自带排序方法的函数式类时total_ordering同样能正确检测到继承的方法并完成合成from functools import total_ordering def lt_impl(self, other) - bool: return True def eq_impl(self, other) - bool: return True # 带 __lt__ 的函数式基类 OrderedBase type(OrderedBase, (), {__lt__: lt_impl}) total_ordering class Ordered(OrderedBase): def __eq__(self, other: object) - bool: return True o1 Ordered() o2 Ordered() reveal_type(o1 o2) # revealed: bool继承的 __lt__ reveal_type(o1 o2) # revealed: bool合成 reveal_type(o1 o2) # revealed: bool合成 reveal_type(o1 o2) # revealed: bool合成反之如果动态基类没有任何排序方法则报错from functools import total_ordering NoOrderBase type(NoOrderBase, (), {}) total_ordering # error: [invalid-total-ordering] class NoOrder(NoOrderBase): def __eq__(self, other: object) - bool: return True动态命名空间保守地不报错最后一个边界情况当type()构造的类带有动态命名空间来自外部dict时类型检查器无法得知命名空间里是否有排序方法因此采取保守策略——不报错from functools import total_ordering from typing import Any def f(ns: dict[str, Any]): # 动态命名空间——可能包含排序方法 DynamicBase type(DynamicBase, (), ns) # 无错误动态命名空间可能包含 __lt__ 等排序方法 total_ordering class Ordered(DynamicBase): def __eq__(self, other: object) - bool: return True # 函数调用形式同样不报错 OrderedDirect total_ordering(type(OrderedDirect, (), ns))这一宁可信其有的设计避免了误报既然无法静态证明排序方法不存在就信任命名空间可能提供它。这体现了类型检查器在处理动态特性时的务实取舍——把不确定性转化为允许通过而不是武断地报错。小结一份测试文档映射出的完整实现链路从这份 mdtest 可以梳理出ty对functools.total_ordering的完整支持脉络识别class.rs通过KnownFunction::TotalOrdering识别装饰器crates/ty_python_semantic/src/types/infer/builder/class.rs L186-L192在StaticClassLiteral上置位total_orderingcrates/ty_python_semantic/src/types/class/static_literal.rs L96-L98选根total_ordering_root_method按__lt__ __le__ __gt__ __ge__优先级、沿 MRO排除object、兼容动态类选择 root 方法同文件 L309-L357合成own_synthesized_member在方法尚未定义于 MRO且存在 root 方法时克隆 root 签名、返回类型与bool取并集、保留全部重载同文件 L1646-L1700校验装饰器形式在post_inference/static_class.rsL643-L653 检查 MRO 并上报invalid-total-ordering函数调用形式在 crates/ty_python_semantic/src/types/function.rs L3030-L3050 走同一has_ordering_method_in_mro校验规则说明见 crates/ty_python_semantic/resources/lint_docs/invalid-total-ordering.md。对于想要在 Ruff 仓库中继续深入研究的读者建议从三个入口出发以本文的 mdtest 文档作为行为总纲crates/ty_python_semantic/resources/mdtest/decorators/total_ordering.md以static_literal.rs的own_synthesized_member与total_ordering_root_method作为核心实现再配合invalid-total-ordering规则文档验证诊断语义。total_ordering的运行时行为还可对照 typeshed 存根 crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi 理解其函数签名。这三者合在一起即可完整还原静态类型检查器如何在不执行代码的前提下复现一个标准库装饰器的全部语义。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门