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

为类添加方法(monkey patching/继承)时如何通过mypy检查

问题背景

我正在使用包含条目类与条目集合类的外部代码,例如Apple和BasketofApples类,希望为这些类添加自定义方法,同时保证代码通过mypy类型检查。

已尝试的存在问题的方案

方案1:猴子补丁(Monkey Patching)

直接给类动态挂载方法会触发mypy报错,示例代码如下:

from typing import List


class Apple:
    pass


class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None:
        self.apples = apples


def _get_id_str(self: Apple) -> str:
    return str(id(self))


Apple.get_id_str = _get_id_str  # error: "Type[Apple]" has no attribute "get_id_str"

_ = Apple().get_id_str()  # error: "Apple" has no attribute "get_id_str"


def _get_id_strs(self: BasketOfApples) -> List[str]:
    return [x.get_id_str() for x in self.apples]  # error: "Apple" has no attribute "get_id_str"


BasketOfApples.get_id_strs = _get_id_strs  # error: "Type[BasketOfApples]" has no attribute "get_id_strs"

_ = BasketOfApples([Apple()]).get_id_strs()  # error: "BasketOfApples" has no attribute "get_id_strs"

方案2:常规继承

直接写子类追加方法同样会触发类型不兼容警告,示例代码如下:

from typing import List


class Apple:
    pass


class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None:
        self.apples = apples


class SonOfApple(Apple):
    def get_id_str(self) -> str:
        return str(id(self))


class SonOfBasketOfApples(BasketOfApples):
    def __init__(self, apples: List[SonOfApple]) -> None:
        super().__init__(apples)  # error: ... incompatible type "List[SonOfApple]"; expected "List[Apple]"

    def get_id_str(self) -> List[str]:
        return [x.get_id_str() for x in self.apples]  # error: "Apple" has no attribute "get_id_str"

方案3:重写子类初始化方法(不调用super)

这个版本可以消除警告,但需要完全重写SonOfBasketOfApples.__init__方法且不调用父类构造,如果父类逻辑复杂,维护成本极高,示例代码如下:

from typing import List


class Apple:
    pass


class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None:
        self.apples = apples


class SonOfApple(Apple):
    def get_id_str(self) -> str:
        return str(id(self))


class SonOfBasketOfApples(BasketOfApples):
    def __init__(self, apples: List[SonOfApple]) -> None:
        self.apples = apples

    def get_id_strs(self) -> List[str]:
        return [x.get_id_str() for x in self.apples]

方案4:修改父类源码加泛型(仅有权限改源码时可用)

如果可以修改父类源码,用泛型+TypeVar可以优雅实现,但实际场景中外部类源码无法修改,示例代码如下:

from typing import List, Generic, TypeVar


class Apple:
    pass


T = TypeVar("T", bound=Apple)


class BasketOfApples(Generic[T]):
    def __init__(self, apples: List[T]) -> None:
        self.apples = apples


class SonOfApple(Apple):
    def get_id_str(self) -> str:
        return str(id(self))


U = TypeVar("U", bound=SonOfApple)


class SonOfBasketOfApples(BasketOfApples[U]):
    def get_id_strs(self) -> List[str]:
        return [x.get_id_str() for x in self.apples]

可行的低维护成本方案

你提到的修改*.pyi类型存根文件是这类场景下的标准做法,除此之外还有两种适配方案:

方案A:编写局部类型存根文件

不需要修改外部库源码,只需要在项目中补充对应外部类的类型声明:

  1. 配置mypy的mypy_path指向你存放存根的目录,或者直接将存根目录放在项目根目录
  2. 按照外部库的模块路径创建对应的.pyi文件,比如外部类来自external_lib.fruits模块,就创建external_lib/fruits.pyi
  3. 在存根文件中保留原有类的必要类型定义,同时补充你追加的方法声明,示例:
# external_lib/fruits.pyi
from typing import List

class Apple:
    def get_id_str(self) -> str: ...

class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None: ...
    apples: List[Apple]
    def get_id_strs(self) -> List[str]: ...

存根编写完成后,mypy会优先读取存根中的类型定义,不会再报属性不存在的错误,运行时的猴子补丁逻辑可以正常生效。原有类的方法、属性声明可以用stubgen工具自动生成后再补充自定义方法,不需要手动全量编写。

方案B:利用TYPE_CHECKING常量做局部类型扩展

如果不想维护独立的存根文件,可以在代码中加入仅类型检查阶段生效的类扩展声明,运行时不会产生任何影响:

from typing import List, TYPE_CHECKING

# 导入外部类
class Apple:
    pass

class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None:
        self.apples = apples

# 仅mypy检查时会读取该代码块,运行时不执行
if TYPE_CHECKING:
    class Apple:
        def get_id_str(self) -> str: ...
    class BasketOfApples:
        apples: List[Apple]
        def get_id_strs(self) -> List[str]: ...

# 运行时正常打猴子补丁
def _get_id_str(self: Apple) -> str:
    return str(id(self))
Apple.get_id_str = _get_id_str

def _get_id_strs(self: BasketOfApples) -> List[str]:
    return [x.get_id_str() for x in self.apples]
BasketOfApples.get_id_strs = _get_id_strs

方案C:继承时做轻量类型转换,避免重写父类逻辑

如果不想使用猴子补丁,坚持用继承的方式实现,可以通过重写属性类型标注+轻量类型转换解决类型不兼容问题,不需要完全重写__init__:

from typing import List, cast

class Apple:
    pass

class BasketOfApples:
    def __init__(self, apples: List[Apple]) -> None:
        self.apples = apples

class SonOfApple(Apple):
    def get_id_str(self) -> str:
        return str(id(self))

class SonOfBasketOfApples(BasketOfApples):
    apples: List[SonOfApple]  # 重写集合属性的类型标注
    def __init__(self, apples: List[SonOfApple]) -> None:
        super().__init__(cast(List[Apple], apples))  # cast仅做类型声明,运行时无额外开销
    def get_id_strs(self) -> List[str]:
        return [x.get_id_str() for x in self.apples]

注意:Python的List是不变类型,直接将List[SonOfApple]传给要求List[Apple]的父类构造会触发类型错误,cast不会修改运行时逻辑,仅用于告知类型检查器类型兼容。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 23:13:00