为什么用联合类型A|B|C约束泛型参数时mypy会报类型不兼容错误?
这个问题问得非常到位!我来帮你拆解mypy在这里的行为逻辑,以及为什么不同的约束写法会产生截然不同的结果:
一、为什么T: A | B | C会触发类型错误?
当你用T: A | B | C作为泛型约束时,mypy对这个约束的理解是:T可以是A、B、C中的任意一个,甚至是它们的联合类型。在泛型函数内部,mypy没办法将elem.dup()的返回类型统一为list[T]——它会把elem.dup()的返回值推断成list[A] | list[B] | list[C],而你声明的变量类型是list[T]。
这里的核心矛盾是:list[A] | list[B] | list[C]和list[T]在mypy的类型系统里不是等价的。前者是“多种不同列表类型的联合”,后者是“对应当前具体T的同质列表”。mypy没办法自动把联合列表类型提升为泛型同质列表类型,因为它不确定T到底是哪一个具体类型(毕竟约束允许T是联合类型本身)。
二、为什么换成T: HasDup或T: (A, B, C)就没问题?
我们分别来看这两种写法的逻辑:
1. 用Protocol约束T: HasDup
HasDup是一个结构类型的Protocol,它定义了“任何实现了dup() -> list[Self]的类型都属于这个Protocol”。当你把泛型约束设为HasDup时,mypy会利用结构子类型匹配的特性:它知道不管T是A、B还是C,dup()方法返回的一定是list[T](因为Protocol里的Self会绑定到具体的T类型)。所以mypy可以100%确定elem.dup()的返回类型就是list[T],和变量类型完全匹配,自然不会报错。
2. 用元组约束T: (A, B, C)
这种写法是mypy的受限联合约束语法,和A | B | C有本质区别:它告诉mypy,T必须是A、B、C中的某一个具体类型,而不是它们的联合。在这种约束下,mypy会对泛型函数做“单类型分支”的推断——它知道每个process调用中的T都是单一具体类型(比如A、B或C),所以elem.dup()的返回类型就是对应的list[A]/list[B]/list[C],完全匹配list[T]的类型声明。
三、你的疑问:这是mypy的限制吗?A|B|C是不是HasDup的子类型?
首先明确:A | B | C确实是HasDup的子类型(从结构子类型的角度,每个类型都实现了dup()方法)。但问题的根源不是子类型关系,而是mypy对不同泛型约束的处理逻辑差异:
T: A | B | C:mypy将其视为“T可以是这些类型的任意组合(包括联合)”,所以在泛型内部无法做“单一具体类型”的推断。T: HasDup/T: (A, B, C):mypy将其视为“T是一个符合条件的单一具体类型”,所以可以正确关联dup()的返回类型和list[T]。
这不算mypy的bug,而是联合类型作为泛型约束时的设计行为——联合类型的语义更偏向于“多类型的动态切换”,而元组约束/Protocol则偏向于“单类型的静态抽象”。
给你的小建议
如果你的泛型函数只需要处理A、B、C这三个具体类型中的某一个,优先用T: (A, B, C)的约束写法;如果未来可能有更多实现dup()方法的类型,用HasDup Protocol会更灵活,扩展性更好。
备注:内容来源于stack exchange,提问作者Дмитрий Воронецкий

