Python运算符dunder方法类型注解的最佳实践是什么
问题背景
假设定义了一个支持+运算的类A,A的实例之间相加后返回的结果仍然是A的实例,示例代码如下:
class A: ... def __add__(self, other): if not isinstance(other, A): return NotImplemented else: return A(self._value + other._value) # 具体实现逻辑不影响讨论
给这个__add__方法加类型注解时,目前能想到的三种方案都存在明显缺陷:
公共前置说明
为了简化示例,下面的演示统一使用Python 3.11引入的Self类型,这个选择不影响核心问题的讨论,前置导入代码如下:
from types import NotImplementedType from typing import Any, Self, overload
方案1
def __add__(self, other: Self) -> Self:
对这个方案的顾虑:
- 方法实际可能返回
NotImplemented,返回值只标Self好像不完整 - 正如Richard Ambler在评论中提到的,
other参数实际可能接收任意类型的对象,直接标Self好像不符合实际情况
方案2
def __add__(self, other: Any) -> Self | NotImplementedType:
这个方案的问题很明确:类型检查器会把A() + A()的结果推断为A | NotImplementedType类型,后面写类似obj = A() + A(); f(obj)的代码时,只要f要求传入A类型,就会触发*“obj可能是NotImplemented”*的无意义类型警告。
方案3
@overload def __add__(self, other: A) -> Self: ... @overload def __add__(self, other: Any) -> Self | NotImplementedType: ... def __add__(self, other): ... # 具体实现
这个方案逻辑上看似没问题,但实际用起来很麻烦:
- 旧版本mypy对
Self类型的支持不完善,没法正常做类型检查 - 需要写很多冗余的样板代码,运算符类型标注本来是非常通用的常规需求,写这么多代码性价比太低
官方最佳实践
直接用方案1的写法就对了,这也是Python官方typeshed仓库为所有内置类型运算符方法采用的标注标准。所有主流类型检查器(mypy 1.0+、pyright、pytype)都对二元运算的双下方法做了特殊适配,根本不需要你额外处理NotImplemented的类型逻辑:
- 类型检查器默认知道
__add__这类运算符方法可能返回NotImplemented,不会因为你没在返回类型里写NotImplementedType就报类型错误。当你写a + b这样的表达式时,检查器会自动处理NotImplemented分支:如果b的类型和__add__标注的入参类型不匹配,会自动尝试调用b的反向运算方法__radd__,只有两边都返回NotImplemented时,才会判定这个运算表达式存在类型错误。 - 不需要把
other参数标成Any,如果你的类只支持和同类型实例相加,直接标Self就可以。类型检查器不会因为传入其他类型的对象就直接报错,它会完全遵循Python原生的运算符分派逻辑做校验,不会干扰正常的运行时行为。 - 只有当你的类需要支持和多种不同类型对象做相加运算时(比如既支持和
A实例相加,也支持和整数相加),才需要用到@overload,分别给每种支持的入参类型标注对应的返回值就行,不需要额外加一个入参为Any、返回Self | NotImplementedType的重载分支。
如果要兼容不支持Self类型的旧版类型检查器,可以用绑定到当前类的TypeVar实现完全等价的效果,示例如下:
from typing import TypeVar T = TypeVar("T", bound="A") class A: ... def __add__(self: T, other: T) -> T: if not isinstance(other, A): return NotImplemented else: return A(self._value + other._value)
内容的提问来源于stack exchange,提问作者fonini
相关产品推荐
相关产品推荐

