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

如何让Python子类__call__继承run方法的类型提示?

问题:让子类__call__继承run方法的类型提示

需求概述

  • 在ParentClass的子类中实现run方法,子类实例调用时(即触发__call__)自动执行run,同时希望run的参数、返回值类型提示能直接应用到__call__的调用处
  • 子类仅需定义run方法,无需手动实现__call__,也不想在子类中指定元类
  • 要在__call__执行前后插入统一的包装逻辑

期望的子类定义与使用方式

子类定义示例

from abc import abstractmethod

class ParentClass:
    # 父类承载复杂实现,让子类保持简洁
    @abstractmethod
    def run(self):
        raise NotImplementedError()

class MyClass(ParentClass):
    def run(self, a: int = 5) -> int:
        print("my", a)
        return 1

class MyClass2(ParentClass):
    def run(self, b: str = "", c: bool = False) -> int:
        print("my2", b, c)
        return 2

调用示例

my = MyClass()
my(a=6) # 打印`my 6`并返回`1`,期望此处能看到`a`的类型提示

my2 = MyClass2()
my2(b="example", c=True) # 打印`my2 example True`并返回`2`,期望此处能看到`b`、`c`的类型提示

已尝试的方案(存在类型提示丢失问题)

from abc import abstractmethod
from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Self, Any, cast
from __future__ import annotations

P = ParamSpec("P")
T = TypeVar("T")

class ParentMeta(type):
    def __new__(cls: Self, name: str, bases: tuple, namespace: dict[str, Any]) -> ParentMeta:
        def wrapper(func: Callable[P, T]) -> Callable[P, T]:
            @wraps(func)
            def inner(self, *args: P.args, **kwargs: P.kwargs) -> T:
                print("--pre--")
                ret = func(self, *args, **kwargs)
                print("--post--")
                return ret
            return inner
        
        if "run" in namespace:
            namespace["__call__"] = wrapper(namespace["run"])
            del namespace["run"]
        
        return cast(ParentMeta, super().__new__(cls, name, bases, namespace))
    
class ParentClass(object, metaclass=ParentMeta):
    @abstractmethod
    def run(self):
        raise NotImplementedError()

class MyClass(ParentClass):
    def run(self, a: int = 5) -> int:
        print("my", a)
        return 1

my = MyClass()
my(a=6)
# 问题:IDE(VSCode)中输入`my(`时,无法看到`a`的参数提示

对比方案(手动定义__call__能正常获取提示,但不符合需求)

from abc import abstractmethod
from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Self, Any, cast
from __future__ import annotations

P = ParamSpec("P")
T = TypeVar("T")

class ParentMeta(type):
    def __new__(cls: Self, name: str, bases: tuple, namespace: dict[str, Any]) -> ParentMeta:
        def wrapper(func: Callable[P, T]) -> Callable[P, T]:
            @wraps(func)
            def inner(self, *args: P.args, **kwargs: P.kwargs) -> T:
                print("--pre--")
                ret = func(self, *args, **kwargs)
                print("--post--")
                return ret
            return inner
        
        if "__call__" in namespace:
            namespace["__call__"] = wrapper(namespace["__call__"])
        
        return cast(ParentMeta, super().__new__(cls, name, bases, namespace))
    
class ParentClass(object, metaclass=ParentMeta):
    @abstractmethod
    def __call__(self):
        raise NotImplementedError()

class MyClass(ParentClass):
    def __call__(self, a: int = 5) -> int:
        print("my", a)
        return 1

my = MyClass()
my(a=6) # 输入`my(`时能正常看到`(a: int = 5) -> int`的提示

解决方案:利用泛型+ParamSpec实现类型绑定

通过泛型父类将__call__的签名与run方法动态绑定,同时保留元类的包装逻辑,让类型检查器能自动识别__call__的参数提示。

from abc import abstractmethod
from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Generic, Any, cast
from __future__ import annotations

P = ParamSpec("P")
T = TypeVar("T")

class ParentMeta(type):
    def __new__(cls, name: str, bases: tuple, namespace: dict[str, Any]) -> ParentMeta:
        def wrapper(func: Callable[P, T]) -> Callable[P, T]:
            @wraps(func)
            def inner(self, *args: P.args, **kwargs: P.kwargs) -> T:
                print("--pre--")
                ret = func(self, *args, **kwargs)
                print("--post--")
                return ret
            return inner
        
        # 处理run方法:包装后替换为__call__,同时保留原run供类型检查器识别
        if "run" in namespace:
            run_func = namespace["run"]
            namespace["__call__"] = wrapper(run_func)
        
        return cast(ParentMeta, super().__new__(cls, name, bases, namespace))

# 泛型父类,将__call__的签名与run的参数(P)、返回值(T)绑定
class ParentClass(Generic[P, T], metaclass=ParentMeta):
    @abstractmethod
    def run(self, *args: P.args, **kwargs: P.kwargs) -> T:
        raise NotImplementedError()
    
    # 显式定义__call__的类型提示,指向run的签名
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
        return self.run(*args, **kwargs)

# 子类实现
class MyClass(ParentClass[P, int]):
    def run(self, a: int = 5) -> int:
        print("my", a)
        return 1

class MyClass2(ParentClass[P, int]):
    def run(self, b: str = "", c: bool = False) -> int:
        print("my2", b, c)
        return 2

# 使用验证
my = MyClass()
my(a=6) # IDE中能正常看到a的参数提示和返回值类型

my2 = MyClass2()
my2(b="example", c=True) # IDE中能看到b、c的参数提示

方案说明

  1. 泛型绑定:通过Generic[P, T]让父类的__call__签名与run方法的参数、返回值动态绑定,类型检查器能自动推导子类run的签名到__call__上。
  2. 元类逻辑:元类在创建子类时,将run方法包装后替换为__call__,保留原run方法避免类型检查器报错。
  3. 类型提示兼容:父类中显式定义__call__的类型提示,确保IDE能识别参数信息。

备选方案:使用Annotated简化泛型写法

如果觉得泛型写法繁琐,可以用Annotated配合类型别名简化:

from abc import abstractmethod
from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Generic, Annotated, Any, cast
from __future__ import annotations

P = ParamSpec("P")
T = TypeVar("T")
RunSignature = Annotated[Callable[P, T], "run method signature"]

class ParentMeta(type):
    def __new__(cls, name: str, bases: tuple, namespace: dict[str, Any]) -> ParentMeta:
        def wrapper(func: Callable[P, T]) -> Callable[P, T]:
            @wraps(func)
            def inner(self, *args: P.args, **kwargs: P.kwargs) -> T:
                print("--pre--")
                ret = func(self, *args, **kwargs)
                print("--post--")
                return ret
            return inner
        
        if "run" in namespace:
            namespace["__call__"] = wrapper(namespace["run"])
        
        return cast(ParentMeta, super().__new__(cls, name, bases, namespace))

class ParentClass(Generic[P, T], metaclass=ParentMeta):
    @abstractmethod
    def run(self, *args: P.args, **kwargs: P.kwargs) -> T:
        raise NotImplementedError()
    
    __call__: RunSignature

class MyClass(ParentClass[P, int]):
    def run(self, a: int = 5) -> int:
        print("my", a)
        return 1

my = MyClass()
my(a=6) # 同样能获取类型提示

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 16:09:53