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

如何用装饰器标记类实现指定Protocol?类型检查失效问题排查

问题描述

我希望定义一个类装饰器为类添加属性,进而通过用于类型检查的Protocol来检测该属性。我尝试了如下代码:

from dataclasses import dataclass
from typing import Protocol, Any

class Canine(Protocol):
    __canine__: Any

def make_canine(cls):
    setattr(cls, "__canine__", True)
    return cls

@make_canine
@dataclass
class Dog:
    name: str
    weight: float
    colour: str
    good_dog: bool = True

@dataclass
class Cat:
    name: str
    weight: float
    colour: str
    good_cat: bool = True


def play_fetch(animal: Canine):
    print(f"Playing fetch with {animal}...")

clifford = Dog('Clifford', 1000, 'Red')
garfield = Cat('Garfield', 10, 'Orange')

play_fetch(Dog)  # 我预期这能通过类型检查
play_fetch(Cat)  # 我预期这会检查失败

play_fetch(clifford)  # 我认为这也能通过,但不确定?
play_fetch(garfield)  # 我预期这会检查失败

print(Dog.__canine__)  # 输出True
print(Cat.__canine__)  # 抛出AttributeError

但所有我认为能通过类型检查的代码行都未通过,请问这是为什么?

原因分析与解决方案

核心问题

  1. Protocol匹配对象错误
    你定义的Canine Protocol要求实例拥有__canine__属性,但装饰器是把这个属性添加到了类本身,实例并没有该属性。这就导致:

    • play_fetch(clifford)不通过检查:因为clifford实例没有__canine__
    • play_fetch(Dog)不通过检查:类型检查器无法识别装饰器动态添加的类属性,不会认为Dog类符合Canine协议
  2. 装饰器无静态类型信息
    类型检查器(如mypy)不会执行代码,只能通过静态注解分析。你的make_canine装饰器没有添加类型注解,检查器不知道它会给类新增属性,自然无法关联到Canine协议。

修复方案

方案1:让Protocol匹配类(接收类作为参数)

如果play_fetch需要接收类而非实例,把Protocol定义为类类型协议:

from dataclasses import dataclass
from typing import Protocol, Type, TypeVar

T = TypeVar('T')

# 定义类类型的Protocol,要求类本身有__canine__属性
class CanineClass(Protocol):
    __canine__: bool

# 给装饰器添加类型注解,明确返回带属性的类
def make_canine(cls: Type[T]) -> Type[T]:
    setattr(cls, "__canine__", True)
    return cls

@make_canine
@dataclass
class Dog:
    name: str
    weight: float
    colour: str
    good_dog: bool = True

@dataclass
class Cat:
    name: str
    weight: float
    colour: str
    good_cat: bool = True

def play_fetch(animal_cls: CanineClass):
    print(f"Playing fetch with class {animal_cls}...")

play_fetch(Dog)  # 通过类型检查
play_fetch(Cat)  # 检查失败,符合预期

方案2:让实例拥有__canine__属性(接收实例作为参数)

如果play_fetch需要接收实例,修改装饰器给实例添加属性:

from dataclasses import dataclass
from typing import Protocol

# 要求实例拥有__canine__属性
class Canine(Protocol):
    __canine__: bool

def make_canine(cls):
    # 重写类的__init__方法,给实例添加属性
    original_init = cls.__init__
    def new_init(self, *args, **kwargs):
        original_init(self, *args, **kwargs)
        self.__canine__ = True
    cls.__init__ = new_init
    return cls

@make_canine
@dataclass
class Dog:
    name: str
    weight: float
    colour: str
    good_dog: bool = True

@dataclass
class Cat:
    name: str
    weight: float
    colour: str
    good_cat: bool = True

def play_fetch(animal: Canine):
    print(f"Playing fetch with {animal}...")

clifford = Dog('Clifford', 1000, 'Red')
garfield = Cat('Garfield', 10, 'Orange')

play_fetch(clifford)  # 通过类型检查
play_fetch(garfield)  # 检查失败,符合预期

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 20:27:41