【问题标题】:Python generic with union带有联合的 Python 泛型
【发布时间】:2020-09-17 05:29:27
【问题描述】:

我有一个 Document 和 Page 类型,都包含数据和元数据部分。它们看起来一样:

class Document:
    __data: DocumentData
    __meta: DocumentMeta

    def __init__(self, part: Union[DocumentData, DocumentMeta, None] = None, data: Optional[DocumentData] = None,
             meta: Optional[DocumentMeta] = None):
        super().__init__()
        self.data: Optional[DocumentData] = data
        self.meta: Optional[DocumentMeta] = meta

        if part is not None:
            if type(part) == DocumentData:
                data = part
                meta = DocumentMeta()
            elif type(part) == DocumentMeta:
                meta = part
                data = DocumentData()    

class Page:
    __data: PageData
    __meta: PageMeta

    def __init__(self, part: Union[PageData, PageMeta, None] = None, data: Optional[PageData] = None,
             meta: Optional[PageMeta] = None):
        super().__init__()
        self.data: Optional[PageData] = data
        self.meta: Optional[PageMeta] = meta

        if part is not None:
            if type(part) == PageData:
                data = part
                meta = PageMeta()
            elif type(part) == PageMeta:
                meta = part
                data = PageData()

我现在想重构这两种类型以使用泛型类型。我是这样做的:

from typing import Generic, Optional, TypeVar, Union

DataStruct = TypeVar('DataStruct')
MetaStruct = TypeVar('MetaStruct')


class MetaDataStruct(Generic[DataStruct, MetaStruct]):
    __data: DataStruct
    __meta: MetaStruct

    def __init__(
        self,
        part: Union[DataStruct, MetaStruct, None] = None,
        data: Optional[DataStruct] = None,
        meta: Optional[MetaStruct] = None
    ):
        super().__init__()
        self.data: Optional[DataStruct] = data
        self.meta: Optional[MetaStruct] = meta

        if part is not None:
            if type(part) == DataStruct:
                data = part
                meta = MetaStruct()
            elif type(part) == MetaStruct:
                meta = part
                data = DataStruct()


class DocumentData:
    pass


class DocumentMeta:
    pass


class PageData:
    pass


class PageMeta:
    pass


class Document(MetaDataStruct[DocumentData, DocumentMeta]):
    pass


class Page(MetaDataStruct[PageData, PageMeta]):
    pass

现在类型检查几乎没有问题。

  1. 如果 type(part) == DataStruct: 一直返回 False。在运行时,类型(部分)是以下之一:DocumentData、DocumentMeta、PageData、PageMeta。我知道我必须将 type(part) 与 DataStruct 的实际类型进行比较。解决 DataStruct 的运行时类型的正确方法是什么?
    python hints manual 中写道:在运行时,isinstance(x, T) 将引发 TypeError。一般来说,isinstance() 和 issubclass() 不应该与类型一起使用。 我相信这里也有同样的问题。

    我可以使用 type(self).orig_bases[0].args[0] 来推断 DataStruct,但它在概念上是不正确的.它将检索第一个通用参数而不是 DataStruct。因此,如果 MetaDataStruct 基类签名将更改为 class MergedStruct(Struct, Generic[MetaStruct, DataStruct])(交换的 TypeVar 参数),则将检索 MetaStruct 而不是 DataStruct。

  2. 由于某种原因,当我尝试初始化 Document(part=1) 时,它通过了。在实践中,我希望代码会引发 TypeError。

【问题讨论】:

    标签: python generics types subclass type-hinting


    【解决方案1】:

    Python 不会在运行时进行类型检查;您需要使用像mypy 这样的静态分析工具。在您提供的代码上运行 mypy 会显示以下错误:

    22: error: 'DataStruct' is a type variable and only valid in type context
    23: error: Incompatible types in assignment (expression has type "Union[DataStruct, MetaStruct]", variable has type "Optional[DataStruct]")
    24: error: 'MetaStruct' is a type variable and only valid in type context
    25: error: 'MetaStruct' is a type variable and only valid in type context
    26: error: Incompatible types in assignment (expression has type "Union[DataStruct, MetaStruct]", variable has type "Optional[MetaStruct]")
    27: error: 'DataStruct' is a type variable and only valid in type context
    

    如果您添加一行尝试初始化 Document(part=1),您将不会收到运行时错误(您的代码中没有任何内容会引发错误;您的 if/elif 将只是一个空操作),但是您将从 mypy 得到一个类型检查错误,如下所示:

    54: error: Argument "part" to "Document" has incompatible type "int"; expected "Union[DocumentData, DocumentMeta, None]"
    

    您尝试执行的type() 检查(以及等效的isinstance)的问题是TypeVar 没有运行时值,因此您不能将其作为构造函数调用。见:Instantiate a type that is a TypeVar

    解决此问题的一种方法是要求子类提供实际类型:

    from abc import ABC, abstractclassmethod
    from typing import Generic, Optional, Type, TypeVar, Union
    
    DataStruct = TypeVar('DataStruct')
    MetaStruct = TypeVar('MetaStruct')
    
    
    class MetaDataStruct(Generic[DataStruct, MetaStruct], ABC):
    
        @abstractclassmethod
        def _data_type(cls) -> Type[DataStruct]:
            pass
    
        @abstractclassmethod
        def _meta_type(cls) -> Type[MetaStruct]:
            pass
    
        def __init__(
            self,
            part: Union[DataStruct, MetaStruct, None] = None,
            data: Optional[DataStruct] = None,
            meta: Optional[MetaStruct] = None
        ):
            super().__init__()
            self.data: Optional[DataStruct] = data
            self.meta: Optional[MetaStruct] = meta
    
            if part is not None:
                if isinstance(part, self._data_type()):
                    data = part
                    meta = self._meta_type()()
                elif isinstance(part, self._meta_type()):
                    meta = part
                    data = self._data_type()()
    
    
    class DocumentData:
        pass
    
    
    class DocumentMeta:
        pass
    
    
    class Document(MetaDataStruct[DocumentData, DocumentMeta]):
        @classmethod
        def _data_type(cls) -> Type[DocumentData]:
            return DocumentData
    
        @classmethod
        def _meta_type(cls) -> Type[DocumentMeta]:
            return DocumentMeta
    

    上面的类型检查正确(如果你没有在子类中正确实现_data_type_meta_type方法,你会得到mypy错误),并且能够使用类方法在运行时调用适当的构造函数.

    【讨论】:

      【解决方案2】:

      我暂时使用了这个解决方案:

      actual_data_struct = getattr(type(self), '__orig_bases__')[0].__args__[0]
      actual_meta_struct = getattr(type(self), '__orig_bases__')[0].__args__[1]
      
      if part is not None:
          if type(part) == actual_data_struct:
              data = part
              meta = actual_meta_struct()
          elif type(part) == actual_meta_struct:
              meta = part
              data = actual_data_struct()
      

      【讨论】:

        猜你喜欢
        • 2012-02-14
        • 1970-01-01
        • 2020-04-12
        • 1970-01-01
        • 2019-08-05
        • 1970-01-01
        • 1970-01-01
        • 2022-01-22
        • 2018-11-24
        相关资源
        最近更新 更多