ty 类型检查器中的自定义二元运算:dunder 方法解析、反射优先级与 unsupported-operator 诊断
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
本文以 Ruff 仓库内 ty 类型检查器(type checker)的类型推断测试文档为核心,系统讲解 Python 自定义二元运算(binary operations)在静态类型检查中的完整处理规则:类实例如何通过__add__、__mul__、__matmul__等 13 个 dunder 方法获得运算符支持,子类如何继承或通过反射方法(__radd__等)覆盖这些实现,以及操作数不支持运算符时unsupported-operator诊断的触发条件与输出格式。读者将从中掌握 ty 对二元运算的推断语义,理解其底层实现依据(dunder 分派决策树、反射优先级分类、完全限定名诊断),并学会阅读 mdtest 文档化测试的断言语法。
背景:mdtest 文档化测试与 ty 类型检查器
Ruff 仓库中的ty是一个用 Rust 编写的极速类型检查器(见 crates/ty/README.md),其类型推断逻辑全部实现在 crates/ty_python_semantic 中。为了保证类型推断行为可验证、可追溯,ty 采用了一套名为 mdtest 的文档化测试体系:以 Markdown 文件作为测试用例,文件中的 Python 代码块会被提取执行,代码块里的reveal_type(expr) # revealed: X注释用于断言表达式推断出的类型,# error: [code] ...注释用于断言必须出现的诊断,# snapshot: code配合```snapshot代码块则用于校验完整的诊断输出快照。
本文讨论的关联文档 crates/ty_python_semantic/resources/mdtest/binary/custom.md 就是这类 mdtest 用例,主题为"自定义二元运算"(Custom binary operations),与binary/目录下其他用例(booleans.md、classes.md、instances.md、integers.md、tuples.md、unions.md)共同覆盖了二元运算符的类型推断矩阵。这些 mdtest 文件由测试入口 crates/ty_python_semantic/tests/mdtest.rs 通过datatest_stable自动收集运行(匹配./resources/mdtest下所有\.md$文件),因此文档中的每一行断言都是可执行的测试规格。
类实例上的自定义二元运算:13 种运算符全支持
当一个类的实例实现了对应的 dunder 方法时,ty 就能推断出该实例参与二元运算的返回类型。下面的类Yes完整实现了 Python 二元运算符对应的 13 个 dunder 方法,每个方法都返回一个Literal字符串,便于在reveal_type断言中直接观察"调用了哪个运算符":
from typing import Literal class Yes: def __add__(self, other) -> Literal["+"]: return "+" def __sub__(self, other) -> Literal["-"]: return "-" def __mul__(self, other) -> Literal["*"]: return "*" def __matmul__(self, other) -> Literal["@"]: return "@" def __truediv__(self, other) -> Literal["/"]: return "/" def __mod__(self, other) -> Literal["%"]: return "%" def __pow__(self, other) -> Literal["**"]: return "**" def __lshift__(self, other) -> Literal["<<"]: return "<<" def __rshift__(self, other) -> Literal[">>"]: return ">>" def __or__(self, other) -> Literal["|"]: return "|" def __xor__(self, other) -> Literal["^"]: return "^" def __and__(self, other) -> Literal["&"]: return "&" def __floordiv__(self, other) -> Literal["//"]: return "//" class Sub(Yes): ... class No: ... # Yes implements all of the dunder methods. reveal_type(Yes() + Yes()) # revealed: Literal["+"] reveal_type(Yes() - Yes()) # revealed: Literal["-"] reveal_type(Yes() * Yes()) # revealed: Literal["*"] reveal_type(Yes() @ Yes()) # revealed: Literal["@"] reveal_type(Yes() / Yes()) # revealed: Literal["/"] reveal_type(Yes() % Yes()) # revealed: Literal["%"] reveal_type(Yes() ** Yes()) # revealed: Literal["**"] reveal_type(Yes() << Yes()) # revealed: Literal["<<"] reveal_type(Yes() >> Yes()) # revealed: Literal[">>"] reveal_type(Yes() | Yes()) # revealed: Literal["|"] reveal_type(Yes() ^ Yes()) # revealed: Literal["^"] reveal_type(Yes() & Yes()) # revealed: Literal["&"] reveal_type(Yes() // Yes()) # revealed: Literal["//"]这些断言清晰地展示了 ty 的推断结论:二元表达式的返回类型精确等于所选 dunder 方法的返回类型,即Literal["+"]、Literal["-"]……逐一对应。这也是 mdtest 的价值所在——它把"Yes() + Yes()应该推断出Literal["+"]"这样的语义直接固化在文档里,任何回归都会导致测试失败。
继承:子类自动复用父类的 dunder 实现
Sub类通过class Sub(Yes): ...继承自Yes,自身没有定义任何 dunder 方法。Python 的运行时语义决定了运算符方法沿 MRO 向上查找,因此Sub的实例天然拥有Yes的全部运算符能力,ty 的推断结果与父类完全一致:
# Sub inherits Yes's implementation of the dunder methods. reveal_type(Sub() + Sub()) # revealed: Literal["+"] reveal_type(Sub() - Sub()) # revealed: Literal["-"] reveal_type(Sub() * Sub()) # revealed: Literal["*"] reveal_type(Sub() @ Sub()) # revealed: Literal["@"] reveal_type(Sub() / Sub()) # revealed: Literal["/"] reveal_type(Sub() % Sub()) # revealed: Literal["%"] reveal_type(Sub() ** Sub()) # revealed: Literal["**"] reveal_type(Sub() << Sub()) # revealed: Literal["<<"] reveal_type(Sub() >> Sub()) # revealed: Literal[">>"] reveal_type(Sub() | Sub()) # revealed: Literal["|"] reveal_type(Sub() ^ Sub()) # revealed: Literal["^"] reveal_type(Sub() & Sub()) # revealed: Literal["&"] reveal_type(Sub() // Sub()) # revealed: Literal["//"]这一点在实现上与"类继承"一节相互呼应:dunder 方法定义在类上,实例通过所属类(及其基类链)获得运算符支持,ty 在推断时同样遵循方法解析顺序(MRO),因此继承来的实现与直接定义的实现行为完全一致。
未实现 dunder 时的诊断:unsupported-operator
No类没有实现任何 dunder 方法。对No的实例执行任意二元运算时,ty 会推断结果为Unknown,并报告unsupported-operator错误。注意错误消息的精确措辞:当两个操作数类型相同时,使用 "between two objects of type X" 的形式:
# No does not implement any of the dunder methods. # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `No`" reveal_type(No() + No()) # revealed: Unknown # error: [unsupported-operator] "Operator `-` is not supported between two objects of type `No`" reveal_type(No() - No()) # revealed: Unknown # error: [unsupported-operator] "Operator `*` is not supported between two objects of type `No`" reveal_type(No() * No()) # revealed: Unknown # error: [unsupported-operator] "Operator `@` is not supported between two objects of type `No`" reveal_type(No() @ No()) # revealed: Unknown # error: [unsupported-operator] "Operator `/` is not supported between two objects of type `No`" reveal_type(No() / No()) # revealed: Unknown # error: [unsupported-operator] "Operator `%` is not supported between two objects of type `No`" reveal_type(No() % No()) # revealed: Unknown # error: [unsupported-operator] "Operator `**` is not supported between two objects of type `No`" reveal_type(No() ** No()) # revealed: Unknown # error: [unsupported-operator] "Operator `<<` is not supported between two objects of type `No`" reveal_type(No() << No()) # revealed: Unknown # error: [unsupported-operator] "Operator `>>` is not supported between two objects of type `No`" reveal_type(No() >> No()) # revealed: Unknown # error: [unsupported-operator] "Operator `|` is not supported between two objects of type `No`" reveal_type(No() | No()) # revealed: Unknown # error: [unsupported-operator] "Operator `^` is not supported between two objects of type `No`" reveal_type(No() ^ No()) # revealed: Unknown # error: [unsupported-operator] "Operator `&` is not supported between two objects of type `No`" reveal_type(No() & No()) # revealed: Unknown # error: [unsupported-operator] "Operator `//` is not supported between two objects of type `No`" reveal_type(No() // No()) # revealed: Unknown当两个操作数类型不同且均无对应 dunder时,消息切换为 "between objects of type X and Y" 的形式。下面的组合中,No不实现普通 dunder,Yes虽然实现了普通 dunder,却"没有实现任何反射(reflected)方法",因此No() + Yes()这类运算同样失败:
# Yes does not implement any of the reflected dunder methods. # error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `Yes`" reveal_type(No() + Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `Yes`" reveal_type(No() - Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `Yes`" reveal_type(No() * Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `Yes`" reveal_type(No() @ Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `Yes`" reveal_type(No() / Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `Yes`" reveal_type(No() % Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `Yes`" reveal_type(No() ** Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `Yes`" reveal_type(No() << Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `Yes`" reveal_type(No() >> Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `Yes`" reveal_type(No() | Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `Yes`" reveal_type(No() ^ Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `Yes`" reveal_type(No() & Yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `Yes`" reveal_type(No() // Yes()) # revealed: Unknown需要特别强调的是第一组断言中的细节:Yes()出现在右侧时并没有"替身"可用。Python 中No() + Yes()理论上可以尝试调用Yes.__radd__,但Yes没有定义任何__r*__反射方法,ty 因此判定该运算不成立——这正是反射方法在二元运算分派中的关键地位。
反射运算:子类的r*方法覆盖超类普通 dunder
Python 语言规范(数据模型文档中关于object.__radd__的说明)为二元运算定义了完整的分派规则:当右操作数是左操作数的(严格)子类、且提供了不同的反射实现时,反射方法优先。下面的用例把这条规则精确地刻画了出来:Sub继承自Yes(因此拥有__add__等普通 dunder),但同时定义了全套反射方法__radd__、__rsub__、__rmul__等,每个反射方法返回带r前缀的Literal:
from typing import Literal class Yes: def __add__(self, other) -> Literal["+"]: return "+" def __sub__(self, other) -> Literal["-"]: return "-" def __mul__(self, other) -> Literal["*"]: return "*" def __matmul__(self, other) -> Literal["@"]: return "@" def __truediv__(self, other) -> Literal["/"]: return "/" def __mod__(self, other) -> Literal["%"]: return "%" def __pow__(self, other) -> Literal["**"]: return "**" def __lshift__(self, other) -> Literal["<<"]: return "<<" def __rshift__(self, other) -> Literal[">>"]: return ">>" def __or__(self, other) -> Literal["|"]: return "|" def __xor__(self, other) -> Literal["^"]: return "^" def __and__(self, other) -> Literal["&"]: return "&" def __floordiv__(self, other) -> Literal["//"]: return "//" class Sub(Yes): def __radd__(self, other) -> Literal["r+"]: return "r+" def __rsub__(self, other) -> Literal["r-"]: return "r-" def __rmul__(self, other) -> Literal["r*"]: return "r*" def __rmatmul__(self, other) -> Literal["r@"]: return "r@" def __rtruediv__(self, other) -> Literal["r/"]: return "r/" def __rmod__(self, other) -> Literal["r%"]: return "r%" def __rpow__(self, other) -> Literal["r**"]: return "r**" def __rlshift__(self, other) -> Literal["r<<"]: return "r<<" def __rrshift__(self, other) -> Literal["r>>"]: return "r>>" def __ror__(self, other) -> Literal["r|"]: return "r|" def __rxor__(self, other) -> Literal["r^"]: return "r^" def __rand__(self, other) -> Literal["r&"]: return "r&" def __rfloordiv__(self, other) -> Literal["r//"]: return "r//" class No: def __radd__(self, other) -> Literal["r+"]: return "r+" def __rsub__(self, other) -> Literal["r-"]: return "r-" def __rmul__(self, other) -> Literal["r*"]: return "r*" def __rmatmul__(self, other) -> Literal["r@"]: return "r@" def __rtruediv__(self, other) -> Literal["r/"]: return "r/" def __rmod__(self, other) -> Literal["r%"]: return "r%" def __rpow__(self, other) -> Literal["r**"]: return "r**" def __rlshift__(self, other) -> Literal["r<<"]: return "r<<" def __rrshift__(self, other) -> Literal["r>>"]: return "r>>" def __ror__(self, other) -> Literal["r|"]: return "r|" def __rxor__(self, other) -> Literal["r^"]: return "r^" def __rand__(self, other) -> Literal["r&"]: return "r&" def __rfloordiv__(self, other) -> Literal["r//"]: return "r//" # Subclass reflected dunder methods may take precedence over the superclass's regular dunders, # depending on the operands' runtime classes. reveal_type(Yes() + Sub()) # revealed: Literal["r+", "+"] reveal_type(Yes() - Sub()) # revealed: Literal["r-", "-"] reveal_type(Yes() * Sub()) # revealed: Literal["r*", "*"] reveal_type(Yes() @ Sub()) # revealed: Literal["r@", "@"] reveal_type(Yes() / Sub()) # revealed: Literal["r/", "/"] reveal_type(Yes() % Sub()) # revealed: Literal["r%", "%"] reveal_type(Yes() ** Sub()) # revealed: Literal["r**", "**"] reveal_type(Yes() << Sub()) # revealed: Literal["r<<", "<<"] reveal_type(Yes() >> Sub()) # revealed: Literal["r>>", ">>"] reveal_type(Yes() | Sub()) # revealed: Literal["r|", "|"] reveal_type(Yes() ^ Sub()) # revealed: Literal["r^", "^"] reveal_type(Yes() & Sub()) # revealed: Literal["r&", "&"] reveal_type(Yes() // Sub()) # revealed: Literal["r//", "//"]这里是最微妙也最值得理解的一处推断语义:Yes() + Sub()的推断结果是Literal["r+", "+"](联合类型),而不是单一的Literal["r+"]。原因在于:静态类型只能说明Sub是Yes的子类,但"反射方法优先"这一规则取决于操作数在运行时的实际类。左操作数Yes()的静态类型是Yes,它既可能以Yes的身份运行(此时走左操作数的__add__),也可能以某个子类的身份运行(此时右操作数的__radd__优先)。既然两种运行时可能性都存在,ty 便把两条路径的返回类型取并集。mdtest 中的注释也明确写出了这一前提:"depending on the operands' runtime classes"。
无关类之间的反射调用:仍走左操作数普通 dunder
与上面对比的是:当右操作数No与左操作数Yes没有继承关系时,"子类反射优先"的规则不适用,ty 直接采用左操作数的普通 dunder 实现:
# But for an unrelated class, the superclass regular dunders are used. reveal_type(Yes() + No()) # revealed: Literal["+"] reveal_type(Yes() - No()) # revealed: Literal["-"] reveal_type(Yes() * No()) # revealed: Literal["*"] reveal_type(Yes() @ No()) # revealed: Literal["@"] reveal_type(Yes() / No()) # revealed: Literal["/"] reveal_type(Yes() % No()) # revealed: Literal["%"] reveal_type(Yes() ** No()) # revealed: Literal["**"] reveal_type(Yes() << No()) # revealed: Literal["<<"] reveal_type(Yes() >> No()) # revealed: Literal[">>"] reveal_type(Yes() | No()) # revealed: Literal["|"] reveal_type(Yes() ^ No()) # revealed: Literal["^"] reveal_type(Yes() & No()) # revealed: Literal["&"] reveal_type(Yes() // No()) # revealed: Literal["//"]注意No在本节中其实实现了全套反射方法(返回Literal["r+"]等),但这些反射方法在此处完全不被采用——因为反射方法只有在"右操作数是左操作数子类"或"左操作数不支持该运算"时才可能被调用,而这里左操作数Yes自身就支持运算。这精确复现了 Python 运行时"左操作数优先"的默认分派逻辑。
类对象(class objects)上的二元运算:始终不支持
dunder 方法定义在类体中,只对该类的实例生效,对类对象本身无效——要让类对象支持运算符,dunder 必须定义在类的类型(即元类type)上。因此下面的运算全部报错,reveal_type的结果是Unknown,并附带# snapshot: unsupported-operator快照校验:
from typing import Literal class Yes: def __add__(self, other) -> Literal["+"]: return "+" class Sub(Yes): ... class No: ... # snapshot: unsupported-operator reveal_type(Yes + Yes) # revealed: Unknownerror[unsupported-operator]: Unsupported `+` operation --> src/mdtest_snippet.py:11:13 | 11 | reveal_type(Yes + Yes) # revealed: Unknown | ---^^^--- | | | Both operands have type `<class 'Yes'>`# snapshot: unsupported-operator reveal_type(Sub + Sub) # revealed: Unknownerror[unsupported-operator]: Unsupported `+` operation --> src/mdtest_snippet.py:13:13 | 13 | reveal_type(Sub + Sub) # revealed: Unknown | ---^^^--- | | | Both operands have type `<class 'Sub'>`# snapshot: unsupported-operator reveal_type(No + No) # revealed: Unknownerror[unsupported-operator]: Unsupported `+` operation --> src/mdtest_snippet.py:15:13 | 15 | reveal_type(No + No) # revealed: Unknown | --^^^-- | | | Both operands have type `<class 'No'>`快照清晰地展示了诊断的两个组成部分:标题行Unsupported+operation,以及主注解Both operands have type<class 'Yes'>``。注意这里类型显示为类对象形式<class 'Yes'>(元类视角下的类类型),而不是实例类型——这正是"类对象不支持运算符"语义的直接体现。
即使通过返回type[Yes]的函数间接得到类对象,运算依然被拒绝,错误消息中的类型也换成type[Yes]形式:
from typing import Literal class Yes: def __add__(self, other) -> Literal["+"]: return "+" class Sub(Yes): ... class No: ... def yes() -> type[Yes]: return Yes def sub() -> type[Sub]: return Sub def no() -> type[No]: return No # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Yes]`" reveal_type(yes() + yes()) # revealed: Unknown # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Sub]`" reveal_type(sub() + sub()) # revealed: Unknown # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[No]`" reveal_type(no() + no()) # revealed: Unknown函数字面量上的二元运算:全部拒绝
函数对象同样不支持任何二元运算符——函数类function的元类链上没有定义这些 dunder。对同一函数对象f应用 13 种运算符,ty 一律报unsupported-operator,且错误消息用类型显示规则把函数呈现为def f() -> Unknown(函数类型的可读形式):
def f(): pass # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f + f) # revealed: Unknown # error: [unsupported-operator] "Operator `-` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f - f) # revealed: Unknown # error: [unsupported-operator] "Operator `*` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f * f) # revealed: Unknown # error: [unsupported-operator] "Operator `@` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f @ f) # revealed: Unknown # error: [unsupported-operator] "Operator `/` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f / f) # revealed: Unknown # error: [unsupported-operator] "Operator `%` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f % f) # revealed: Unknown # error: [unsupported-operator] "Operator `**` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f**f) # revealed: Unknown # error: [unsupported-operator] "Operator `<<` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f << f) # revealed: Unknown # error: [unsupported-operator] "Operator `>>` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f >> f) # revealed: Unknown # error: [unsupported-operator] "Operator `|` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f | f) # revealed: Unknown # error: [unsupported-operator] "Operator `^` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f ^ f) # revealed: Unknown # error: [unsupported-operator] "Operator `&` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f & f) # revealed: Unknown # error: [unsupported-operator] "Operator `//` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f // f) # revealed: Unknown不同模块中同名类的诊断:使用完全限定名
最后一种边界情况:当两个类未限定名相同但来自不同模块时,诊断消息必须能够区分它们。ty 的做法是在这种情况下改用完全限定名(fully qualified name)。mod1.py与mod2.py各定义了一个A类,mod2.py中执行A() + mod1.A():
mod1.py:
class A: ...mod2.py:
import mod1 class A: ... # snapshot: unsupported-operator A() + mod1.A()error[unsupported-operator]: Unsupported `+` operation --> src/mod2.py:6:1 | 6 | A() + mod1.A() | ---^^^-------- | | | | | Has type `mod1.A` | Has type `mod2.A`快照中两个操作数的注解分别显示为Has typemod1.A与 `Has type `mod2.A。这里不再使用简短的A,而是带上模块前缀,保证诊断信息无歧义;同时因为两个类不同,诊断采用"Has type"逐操作数注解的形式,而不是"Both operands have type"的同类型合并形式。
源码实现:二元运算推断与 dunder 分派
理解了上述全部语义之后,我们来看 ty 是如何在 Rust 中实现这些行为的。二元表达式的推断入口位于 crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs 的infer_binary_expression:它先分别推断左右操作数的类型,再调用infer_binary_expression_type尝试解析运算符;若解析失败,则调用report_unsupported_binary_operation报告诊断并把表达式类型置为Unknown(这正是所有# revealed: Unknown断言的来源)。
核心分派决策树:try_call_bin_op_with_policy
真正的 dunder 分派逻辑在 crates/ty_python_semantic/src/types/call.rs 的try_call_bin_op_with_policy(约 L211 起),它把 Python 数据模型文档中的规则直接翻译成了实现,源码注释引用的决策树为:
- 如果右操作数是左操作数的(严格)子类,且提供了与左操作数不同的
__rop__实现,则优先调用反射方法; - 否则,如果左操作数实现了
__op__,调用它; - 否则,如果左右操作数类型不同,且右操作数实现了
__rop__,调用它; - 否则,判定
NotSupported,触发unsupported-operator诊断。
这与文档中三组用例完全对应:Sub是Yes的子类且提供不同反射实现 → 反射方法参与解析(Literal["r+", "+"]);No与Yes无关 → 直接走左操作数__add__(Literal["+"]);两侧都无实现 →NotSupported(Unknown+ 诊断)。
反射优先级的三态分类:Never / Possibly / Definitely
决策树第一分支依赖reflected_method_priority(同样位于 call.rs,约 L84)对反射方法优先级进行三态分类:
- Never:左右静态类型相同,或右操作数静态上不是左操作数的子类;
- Definitely:右操作数是左操作数的子类,且左操作数具有精确运行时类(如
int字面量,其运行时类必然是int,因此IntFlag这类操作数必然是其严格子类),此时反射方法必然优先; - Possibly:左操作数静态类型是基类
Base,其实例运行时可能是子类Child,因此"反射方法优先"只能被条件性确认。
这正是Yes() + Sub()推断出联合类型Literal["r+", "+"]的根因:优先级为Possibly时,ty 把"反射方法成功调用"与"左操作数普通 dunder 成功调用"两条路径的结果取并集(源码中通过Bindings::from_union合并两个分支的绑定)。若优先级为Definitely,则只保留反射分支。反射方法是否"不同"通过比较右操作数__rop__成员与左操作数同一成员的来源等价性判断,而非简单的同名存在性。
结果缓存与约束展开
try_call_bin_op_result(call.rs,约 L164)对二元运算的解析结果做了 salsa 查询级别的记忆化缓存,只保留返回类型与弃用(deprecated)函数列表,避免在表达式的多个引用处重复做重载选择。infer_binary_expression_type_impl(binary_expressions.rs)则负责在调用 dunder 之前展开各类特殊类型:联合类型按元素逐个尝试、类型别名(TypeAlias)解包到其值类型、约束 TypeVar(如T: (int, str))逐约束配对求值并合并结果、TypedDict走 PEP 584 的合并路径、NewType失败后回退到具体基类型等。这些特殊分支共同保证了 dunder 分派只发生在"真正需要方法解析"的类型上。
诊断的生成:report_unsupported_binary_operation
unsupported-operator这个 lint 在 crates/ty_python_semantic/src/types/diagnostic.rs(约 L1208)中声明,摘要为 "detects binary, unary, or comparison expressions where the operands don't support the operator",默认级别为Error,并随库内 crates/ty_python_semantic/resources/lint_docs/unsupported-operator.md 提供用户文档——该文档明确说明这类表达式在运行时必然抛出TypeError。诊断的格式化实现在同文件的report_unsupported_binary_operation_impl(约 L5569):当两个操作数类型等价时,使用单条主注解 "Both operands have typeX" 并给出简洁消息 "Operator{op}is not supported between two objects of typeX";否则为每个操作数生成 "Has typeX" 注解,简洁消息变为 "Operator{op}is not supported between objects of typeXandY"。此外,诊断显示还会考虑操作数类型的歧义(DisplaySettings::from_possibly_ambiguous_types),这正是"不同模块同名类显示完全限定名"的实现入口。
小结
通过 crates/ty_python_semantic/resources/mdtest/binary/custom.md 这份文档化测试,可以完整还原 ty 对 Python 自定义二元运算的类型推断契约:
- 实例运算:13 种二元运算符逐一映射到对应的 dunder 方法,推断结果就是所选 dunder 的返回类型;
- 继承复用:子类沿 MRO 继承父类的运算符能力;
- 反射优先:右操作数为左操作数子类且提供不同反射实现时,反射方法可能优先,静态类型无法确定运行时类时结果取两条路径的并集(联合类型);
- 类型边界:dunder 只对实例生效,类对象与函数对象不支持任何二元运算;
- 诊断规范:不支持时报告
unsupported-operator,同类型操作数合并显示,跨模块同名类使用完全限定名消除歧义。
这套契约既有 mdtest 文档的可执行断言,也有 binary_expressions.rs、call.rs、diagnostic.rs 三处源码的直接支撑,是理解 ty 运算符推断语义、乃至扩展新运算符支持时最值得通读的起点。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考