带默认参数的Python typing Protocol类型推导错误求助
我编写了一个执行回调的简单函数,核心逻辑是调用传入的回调函数并返回结果:
def callback(*, arg1: float | None = None) -> float: return arg1 or 1.0 def callback2(*, arg1: int | None = None) -> int: return arg1 or 1 def func(arg1 = callback, arg2 = None): return arg1(arg1=arg2) func(callback) func(callback2)
这些回调函数的arg1参数与返回值类型一致,只能是int或float。最初为func添加无默认参数的类型标注时,用typing.Protocol可以正常通过MyPy检查:
from __future__ import annotations from typing import TypeVar, Protocol _ResultType = TypeVar("_ResultType", int, float) _ContraResultType = TypeVar("_ContraResultType", int, float, contravariant=True) _CoResultType = TypeVar("_CoResultType", int, float, covariant=True) class MyCallback(Protocol[_ContraResultType, _CoResultType]): def __call__(self, *, arg1: _ContraResultType | None) -> _CoResultType: ... def callback(arg1: float | None = None) -> float: return arg1 or 1.0 def callback2(arg1: int | None = None) -> int: return arg1 or 1 def func( arg1: MyCallback[_ResultType, _ResultType], arg2: _ResultType | None = None, ) -> _ResultType: return arg1(arg1=arg2) a: float = func(callback) b: int = func(callback2)
但为arg1添加默认值callback后:
def func( arg1: MyCallback[_ResultType, _ResultType] = callback, arg2: _ResultType | None = None, ) -> _ResultType: return arg1(arg1=arg2)
MyPy抛出错误:
example.py:19: error: Incompatible default for argument "arg1" (default has type "Callable[[Optional[float]], float]", argument has type "MyCallback[int, int]") [assignment]
example.py:19: note: "MyCallback[int, int].call" has type "Callable[[NamedArg(Optional[int], 'arg1')], int]"
Found 1 error in 1 file (checked 1 source file)
如果把默认值换成callback2,则会出现反向的类型不兼容错误。需要调整类型标注,让带默认参数的版本也能正确推导类型。
解决方案1:使用函数重载(@overload)
问题核心是默认回调只能处理float类型,但_ResultType允许int,导致类型不匹配。通过函数重载为func定义两种场景:一种是使用默认callback(返回float),另一种是传入自定义回调(支持int或float)。
调整后的代码:
from __future__ import annotations from typing import TypeVar, Protocol, overload _ResultType = TypeVar("_ResultType", int, float) class MyCallback(Protocol[_ResultType]): def __call__(self, *, arg1: _ResultType | None) -> _ResultType: ... def callback(*, arg1: float | None = None) -> float: return arg1 or 1.0 def callback2(*, arg1: int | None = None) -> int: return arg1 or 1 # 重载1:使用默认回调,返回float @overload def func(arg1: None = None, arg2: float | None = None) -> float: ... # 重载2:传入自定义回调,返回对应类型 @overload def func(arg1: MyCallback[_ResultType], arg2: _ResultType | None = None) -> _ResultType: ... # 实际实现 def func( arg1: MyCallback[_ResultType] | None = None, arg2: _ResultType | None = None, ) -> _ResultType: actual_arg1 = arg1 if arg1 is not None else callback return actual_arg1(arg1=arg2) # 类型检查全部通过 a: float = func() # 使用默认callback b: float = func(callback) c: int = func(callback2) d: float = func(callback, 3.5) e: int = func(callback2, 5)
关键说明:
- 简化Protocol:回调的参数和返回值类型一致,只用单个
_ResultType即可,让Protocol更简洁。 - 重载区分场景:明确无参/默认参数场景的返回类型,同时保留自定义回调的类型推导能力。
- 内部处理默认值:将
arg1的默认值设为None,在函数内部替换为callback,避免标注冲突。
解决方案2:让默认回调兼容int和float
如果业务逻辑允许默认回调处理int类型,可以修改callback的类型标注,让它接受int | float | None的参数并返回float(int可隐式转为float):
from __future__ import annotations from typing import TypeVar, Protocol _ResultType = TypeVar("_ResultType", int, float) class MyCallback(Protocol[_ResultType]): def __call__(self, *, arg1: _ResultType | None) -> _ResultType: ... def callback(*, arg1: int | float | None = None) -> float: return float(arg1) if arg1 is not None else 1.0 def callback2(*, arg1: int | None = None) -> int: return arg1 or 1 def func( arg1: MyCallback[_ResultType] = callback, arg2: _ResultType | None = None, ) -> _ResultType: return arg1(arg1=arg2) # 类型检查全部通过 a: float = func() b: float = func(callback) c: int = func(callback2)
这种方案更简洁,但需要确保默认回调的逻辑能兼容int参数的转换。
内容的提问来源于stack exchange,提问作者maxbachmann

