You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用泛型Protocol装饰函数引发Never相关arg-type类型错误

MyPy报Callable与FunctionToDecorate[Never, Never]不兼容的问题解析

错误成因

核心问题是自定义Protocol未正确使用ParamSpec捕获可变位置参数的签名,导致MyPy无法将传入的具体Callable类型(比如Callable[[int, int], int])与FunctionToDecorate协议匹配。当类型检查器找不到合法的类型匹配关系时,会用Never标记不兼容的部分。

为什么出现Never类型

Never是Python typing中的底部类型,代表不可能存在的类型。当MyPy无法推断出某个位置的合法类型,或者判定传入类型完全不符合协议约束时,就会用Never表示这种不兼容状态。这里因为Protocol没有准确描述无关键字参数、可变位置参数的函数结构,MyPy只能判定传入函数无法匹配协议,因此标注为FunctionToDecorate[Never, Never]。

与ParamSpec的关系

在Python 3.10+中,要准确标注带可变参数的函数(尤其是装饰器需要保留原函数签名时),必须用ParamSpec捕获函数的参数签名信息。如果Protocol没有用ParamSpec绑定可变位置参数,MyPy无法识别具体的参数类型结构,自然无法完成类型匹配。

解决方法

通过结合ParamSpec和TypeVar定义泛型Protocol,明确约束函数的参数结构(无关键字参数、可变位置参数),让MyPy能准确推断类型。

原始错误示例(MRE)

from typing import Protocol, TypeVar, Callable

R = TypeVar('R')

class FunctionToDecorate(Protocol[R]):
    def __call__(self, *args) -> R: ...

def decorator(func: FunctionToDecorate[R]) -> FunctionToDecorate[R]:
    def wrapper(*args):
        print("Decorated")
        return func(*args)
    return wrapper

@decorator
def add(a: int, b: int) -> int:
    return a + b

@decorator
def greet(name: str) -> str:
    return f"Hello {name}"

MyPy错误输出:

error: Argument 1 to "decorator" has incompatible type "Callable[[int, int], int]"; expected "FunctionToDecorate[Never]" [arg-type]
error: Argument 1 to "decorator" has incompatible type "Callable[[str], str]"; expected "FunctionToDecorate[Never]" [arg-type]

修正后的代码

from typing import Protocol, TypeVar, ParamSpec, Callable

# 用ParamSpec捕获位置参数签名,TypeVar捕获返回值类型
P = ParamSpec('P')
R = TypeVar('R')

# 定义泛型Protocol,仅接受位置参数(无关键字参数)
class FunctionToDecorate(Protocol[P, R]):
    def __call__(self, *args: P.args) -> R: ...

def decorator(func: FunctionToDecorate[P, R]) -> FunctionToDecorate[P, R]:
    def wrapper(*args: P.args) -> R:
        print("Decorated")
        return func(*args)
    return wrapper

@decorator
def add(a: int, b: int) -> int:
    return a + b

@decorator
def greet(name: str) -> str:
    return f"Hello {name}"

# 若尝试传入关键字参数,MyPy会直接报错(符合需求)
# add(a=1, b=2)  # 错误:Unexpected keyword argument "a" for "add"

修正要点

  1. 引入ParamSpec('P')捕获原函数的位置参数签名,替代模糊的*args定义;
  2. 将Protocol改为双泛型,同时接受参数签名P和返回值R;
  3. 在装饰器的wrapper函数中,用P.args约束参数类型,确保与原函数签名一致;
  4. 若需严格禁止关键字参数,Protocol的__call__方法不定义**kwargs即可,MyPy会自动拦截带关键字参数的函数。

内容的提问来源于stack exchange,提问作者vmonteco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 19:02:04