为何dict[str, str]无法赋值给Mapping[str | int, str]?映射键类型非协变
dict[str, str]无法赋值给Mapping[str | int, str]? 问题重现
代码示例:
from collections.abc import Mapping def my_fn(m: Mapping[str | int, str]): print(m) d = {"a": "b"} my_fn(d)
mypy 1.16.0和pyright 1.1.400均会报错,以pyright输出为例:
error: Argument of type "dict[str, str]" cannot be assigned to parameter "m" of type "Mapping[str | int, str]" in function "my_fn" "dict[str, str]" is not assignable to "Mapping[str | int, str]" Type parameter "_KT@Mapping" is invariant, but "str" is not the same as "str | int" (reportArgumentType)
疑问:既然MutableMapping[str | int, str]接收dict[str, str]会有修改风险(调用方可能插入int类型键),但Mapping是不可变的,为什么还是不允许这种赋值?
原因解析
核心问题在于**Mapping的键类型参数是**不变(invariant)的,而非协变(covariant)。
虽然Mapping接口只定义了读取操作,不允许修改映射,但静态类型检查的核心是保证函数对参数的所有合法操作都符合类型声明。函数my_fn声明参数m是Mapping[str | int, str],就意味着它可以合法地尝试用int类型的键去访问m的值——比如在函数内部写m[123]或者m.get(123),这些操作在类型层面是被允许的。
但如果传入的是dict[str, str],实际执行时用int键访问要么抛出KeyError,要么返回默认值,这就违背了类型声明的承诺:Mapping[str | int, str]保证“任何str或int类型的键都属于这个映射的键集合(或者至少可以安全访问)”,而dict[str, str]的键集合只包含str类型,无法满足这个承诺。
举个更直观的例子,如果my_fn的实现改成这样:
def my_fn(m: Mapping[str | int, str]): # 类型检查认为这是完全合法的操作 print(m.get(123, "默认值"))
虽然这段代码运行时不会崩溃,但类型系统要提前确保参数能支持函数声明中所有允许的操作。允许dict[str, str]赋值给Mapping[str | int, str],就相当于允许函数对一个不支持int键的映射执行int键的访问,这不符合静态类型检查“提前规避类型风险”的目标。
总结来说:即使Mapping是不可变的,函数对参数的读取操作范围依然受键类型的约束。Mapping[str | int, str]要求参数能接受str和int两种类型的键,而dict[str, str]做不到,所以类型检查器会阻止这种赋值。
内容的提问来源于stack exchange,提问作者Kerrick Staley

