如何用装饰器标记类实现指定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
但所有我认为能通过类型检查的代码行都未通过,请问这是为什么?
原因分析与解决方案
核心问题
Protocol匹配对象错误
你定义的CanineProtocol要求实例拥有__canine__属性,但装饰器是把这个属性添加到了类本身,实例并没有该属性。这就导致:play_fetch(clifford)不通过检查:因为clifford实例没有__canine__play_fetch(Dog)不通过检查:类型检查器无法识别装饰器动态添加的类属性,不会认为Dog类符合Canine协议
装饰器无静态类型信息
类型检查器(如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
相关产品推荐
相关产品推荐

