泛型 类 上的类方法

Classmethods on generic classes

我尝试在通用 class 上调用 class 方法:

from typing import List, Union, TypeVar, Generic
from enum import IntEnum

class Gender(IntEnum):
    MALE = 1
    FEMALE = 2
    DIVERS = 3


T = TypeVar('T')

class EnumAggregate(Generic[T]):
    def __init__(self, value: Union[int, str, List[T]]) -> None:
        if value == '':
            raise ValueError(f'Parameter "value" cannot be empty!')

        if isinstance(value, list):
            self._value = ''.join([str(x.value) for x in value])
        else:
            self._value = str(value)

    def __contains__(self, item: T) -> bool:
        return item in self.to_list

    @property
    def to_list(self) -> List[T]:
        return [T(int(character)) for character in self._value]

    @property
    def value(self) -> str:
        return self._value

    @classmethod
    def all(cls) -> str:
        return ''.join([str(x.value) for x in T])

Genders = EnumAggregate[Gender]

但是如果我打电话

Genders.all()

我收到错误 TypeError: 'TypeVar' object is not iterable。所以 TypeVar T 与 Enum Gender.

没有正确匹配

我该如何解决这个问题?预期的行为是

>>> Genders.all()
'123'

有什么想法吗?或者这是不可能的?

Python 的类型提示系统用于静态类型检查器来验证您的代码,而 T 只是类型系统的 占位符 ,就像模板语言中的槽。它不能用作对特定类型的间接引用。

如果您想生成具体的实现,您需要 subclass 您的通用类型。因为 Genderclass 而不是实例,所以您需要告诉类型系统您打算如何在某处使用 Type[T] ,也是。

因为您还希望能够将 T 用作 Enum()(用 EnumSubclass(int(character)) 调用它),我还会 绑定 typevar;这样类型检查器就会明白 Type[T] 的所有具体形式都是可调用的,并且会产生单独的 T 实例,而且那些 T 实例将 always 有一个 .value 属性:

from typing import ClassVar, List, Union, Type, TypeVar, Generic
from enum import IntEnum

T = TypeVar('T', bound=IntEnum)  # only IntEnum subclasses

class EnumAggregate(Generic[T]):
    # Concrete implementations can reference `enum` *on the class itself*,
    # which will be an IntEnum subclass.
    enum: ClassVar[Type[T]]

    def __init__(self, value: Union[int, str, List[T]]) -> None:
        if not value:
            raise ValueError('Parameter "value" cannot be empty!')

        if isinstance(value, list):
            self._value = ''.join([str(x.value) for x in value])
        else:
            self._value = str(value)

    def __contains__(self, item: T) -> bool:
        return item in self.to_list

    @property
    def to_list(self) -> List[T]:
        # the concrete implementation needs to use self.enum here
        return [self.enum(int(character)) for character in self._value]

    @property
    def value(self) -> str:
        return self._value

    @classmethod
    def all(cls) -> str:
        # the concrete implementation needs to reference cls.enum here
        return ''.join([str(x.value) for x in cls.enum])

使用上面的通用 class,您现在可以创建一个 具体 实现,使用您的 Gender IntEnum 安装到 T 插槽并作为 class 属性:

class Gender(IntEnum):
    MALE = 1
    FEMALE = 2
    DIVERS = 3


class Genders(EnumAggregate[Gender]):
    enum = Gender

为了能够访问 IntEnum subclass 作为 class 属性,我们需要使用 typing.ClassVar[];否则类型检查器必须假定该属性仅在实例上可用。

并且因为 Gender IntEnum subclass 本身就是一个 class,我们需要告诉类型检查器关于这一点,因此使用 typing.Type[].

现在 Gender 具体子 class 工作;使用 EnumAggregate[Gender] 作为基础 class 告诉类型检查器用 T 替换所有地方的 Gender,并且因为实现使用 enum = Gender,类型检查器看到这确实得到了正确的满足并且代码通过了所有检查:

$ bin/mypy so65064844.py
Success: no issues found in 1 source file

你可以调用 Genders.all() 来生成一个字符串:

>>> Genders.all()
'123'

请注意,我不会将枚举值存储为字符串,而是存储为整数。在这里来回转换它没有什么价值,并且您将自己限制在值介于 0 和 9(个位数)之间的枚举。

另一个答案不再有效,至少在 Python 3.10 中是这样。类型注释 ClassVar[Type[T]] 导致 mypy 错误: ClassVar cannot contain type variables 被抛出。这是因为 ClassVar 只能用于 Protocol 和结构子类型,这不是手头问题的最佳答案。

其他答案的以下修改有效:

class EnumAggregate(Generic[T]):
    enum: type[T]

[...]

class Genders(EnumAggregate[Gender]):
    enum = Gender

抽象class变量

我还建议以某种方式使 enum 抽象化,因此实例化 EnumAggregate[Gender] 而不是 Genders 将在实例化时引发错误,而不仅仅是在调用 to_list()all().

这可以通过两种方式完成:检查 __init__ 中的实现:

class EnumAggregate(Generic[T]):
    enum: type[T]
    def __init__ 
    [...]
    if not hasattr(type(self), 'enum'):
        raise NotImplementedError("Implementations must define the class variable 'enum'")

或使用摘要 class 属性,参见 this discussion. This makes mypy happy in several situations, but not Pylance (see here):

class EnumAggregate(Generic[T]):
    @property
    @classmethod
    @abstractmethod
    def enum(cls) -> type[T]: ...

[...]

class Genders(EnumAggregate[Gender]):
    enum = Gender

但是,mypy 和装饰器存在未解决的问题,因此目前存在虚假错误,这些错误将来可能会消失。供参考:

mypy issue 1

mypy issue 2

Discussion whether to deprecate chaining classmethod decorators