在签名中注释特定的数据类子类

Annotating specific dataclass subclass in a signature

from dataclasses import dataclass


@dataclass
class BaseProduct:
    ...


@dataclass
class ProductA(BaseProduct):
    a_specific_id: int


@dataclass
class ProductSubmissionCommand:
    product_id: str
    product: BaseProduct


class AProductRequestSubmitter:
    def __call__(self, job_id: int, cmd: ProductSubmissionCommand):
        ...
        cmd.product.a_specific_id

mypy 错误是: error: "BaseProduct" has no attribute "a_specific_id"

如何正确注释AProductRequestSubmitter.__call__?此 class 特定于 AProduct,还有针对不同产品类型的其他提交者。

是否可以以某种方式使用通用类型或文字值?或者 cast 或 assert 是唯一的方法吗?

有多种方法可以实现您的目标,我将展示我现在想到的那些。

确保运行时类型正确

可能有多个 children 将 BaseProduct 作为他们的 parent 并且类型检查器无法确定地知道,你永远不会通过 ProductSubmissionCommand 具有 product 类型属性的实例,该类型是 BaseProduct 的 child,没有 a_specific_id 属性。为了确保这永远不会发生,您可以使用类型断言开始函数调用,例如:

class AProductRequestSubmitter:
    def __call__(self, job_id: int, cmd: ProductSubmissionCommand):
        if not isinstance(cmd.product, ProductA):
            raise TypeError(f"Only 'ProductA' instances are allowed, not '{cmd.product.__class__.__name__}'")
        ...
        cmd.product.a_specific_id

Pycharm(或任何其他静态类型检查器)现在应该是安静的,因为程序永远不会到达代码的一部分,除非使用正确的类型,否则将访问 a_specific_id .

子类ProductSubmissionCommand

如果您真的打算使用 AProductRequestSubmitter 作为实例,其中 ProductSubmissionCommand 具有类型 ProductAproduct,您可以子类化您的 ProductSubmissionCommand 和类型提示其 product 属性作为 ProductA 实例。

@dataclass
class ProductASubmissionCommand(ProductSubmissionCommand):
    product: ProductA


class AProductRequestSubmitter:
    def __call__(self, job_id: int, cmd: ProductASubmissionCommand):
        ...
        cmd.product.a_specific_id

同样,所有聪明的类型检查员现在都应该明白,cmd 将始终具有 product.a_specific_id 属性。

使用Protocol

使用:

class ProductASubmissionCommand(Protocol):
    product: ProductA

class AProductRequestSubmitter:
    def __call__(self, job_id: int, cmd: ProductASubmissionCommand):
        ...
        cmd.product.a_specific_id

您基本上是在说“接受任何具有 ProductA 类型的名为 product 的属性”。这有点 hacky,让你创建一个协议子类只是为了类型提示这个单一的方法调用,但是,同样,应该与所有静态类型检查器一起工作。

您有 2 种可能的方法。

  1. 使用显式 getattr 调用忽略静态打字

     class AProductRequestSubmitter:
         def __call__(self, job_id: int, cmd: ProductSubmissionCommand):
             ...
             getattr(cmd.product, 'a_specific_id')
    
  2. 使用Generic 专业化 ProductSubmissionCommand 从静态类型的角度来看:

     T = TypeVar("T", bound=BaseProduct)
    
    
     @dataclass
     class ProductSubmissionCommand(Generic[T]):
         product_id: str
         product: T
    
    
     class AProductRequestSubmitter:
         def __call__(self, job_id: int, cmd: ProductSubmissionCommand[ProductA]):
             pass