子类化list时如何正确编写__add__及__iadd__方法的类型标注?
我完全懂你这种“明明逻辑很直观,为啥类型检查器揪着不放”的挫败感!子类化内置list时,__add__、__iadd__这种带泛型重载的魔术方法,确实是mypy的重点“关照”对象,稍不注意就会触发签名不兼容的报错。咱们一步步拆解问题,找到既满足类型检查、又符合你需求的方案。
为什么最初的实现会报错?
内置list的__add__其实有两个重载签名:
- 当和同类型
list[int]相加时,返回list[int] - 当和任意类型
list[_S]相加时,返回包含联合类型元素的list[_S | int]
你最初的代码只实现了其中一个重载,而且返回类型是MyList——虽然MyList是list[int]的子类(符合里氏替换原则),但mypy要求子类必须完整覆盖父类的所有重载签名,不能只挑一个来实现,这才是报错的核心原因。
方案1:保留非泛型MyList,兼容所有重载
如果你不想把MyList改成泛型类,只想让它专门存储int类型元素,那可以通过完整覆盖父类的所有重载来解决问题,同时在非int列表相加时返回普通list(或者根据运行时类型判断返回MyList):
from __future__ import annotations from typing import overload, TypeVar _T = TypeVar("_T") class MyList(list[int]): @overload def __add__(self, other: list[int]) -> MyList: ... @overload def __add__(self, other: list[_T]) -> list[_T | int]: ... def __add__(self, other: list[int] | list[_T]) -> MyList | list[_T | int]: # 调用父类的__add__得到合并后的列表 combined = super().__add__(other) # 如果所有元素都是int,返回MyList,否则返回普通list if all(isinstance(item, int) for item in combined): return MyList(combined) return combined
这样mypy会认可你的签名,因为你完整覆盖了父类的两个重载,返回类型也和父类的签名兼容(MyList是list[int]的子类,list[_T | int]完全匹配父类的返回要求)。
方案2:将MyList改为泛型类(更优雅的长期方案)
如果希望和任意类型列表相加时,返回的依然是MyList而非普通list,那把MyList改成泛型类是更合理的选择——这样类型系统能自动处理联合类型的元素,也完全符合父类的泛型重载逻辑:
from typing import Generic, TypeVar, overload _T = TypeVar("_T") _S = TypeVar("_S") class MyList(list[_T], Generic[_T]): @overload def __add__(self, other: list[_T]) -> MyList[_T]: ... @overload def __add__(self, other: list[_S]) -> MyList[_T | _S]: ... def __add__(self, other: list[_T] | list[_S]) -> MyList[_T] | MyList[_T | _S]: combined = super().__add__(other) return MyList(combined) # 处理__iadd__方法 @overload def __iadd__(self, other: list[_T]) -> MyList[_T]: ... @overload def __iadd__(self, other: list[_S]) -> MyList[_T | _S]: ... def __iadd__(self, other: list[_T] | list[_S]) -> MyList[_T] | MyList[_T | _S]: super().__iadd__(other) return self
这个方案的优势在于:
- 完整覆盖了父类
list的所有__add__和__iadd__重载签名 - 无论相加的列表是什么类型,返回的都是
MyList的对应泛型版本,完全符合你的需求 - 严格遵循里氏替换原则:
MyList[_X]是list[_X]的子类,任何接受list[_X]的场景都能无缝接受MyList[_X]
关于__iadd__的坑
__iadd__是原地修改方法,父类的签名要求它返回Self(即自身实例)。如果你的MyList是非泛型的list[int]子类,当你给它添加非int元素时,虽然运行时实例还是MyList,但类型系统会认为它的元素类型依然是int,这会导致类型不匹配——这也是为什么泛型方案更可靠的原因:泛型MyList能自动更新元素类型的标注,完美适配__iadd__的原地修改逻辑。
验证效果
用上面的泛型方案编写代码后,运行mypy --strict不会再触发任何报错,而且相加操作的返回类型会被正确推断:
MyList[int]() + [1,2]→ 返回MyList[int]MyList[int]() + ["a","b"]→ 返回MyList[int | str]
这样既满足了类型检查的要求,又实现了“返回MyList而非普通list”的业务需求。
备注:内容来源于stack exchange,提问作者LoneCodeRanger

