如何类型提示检索动态枚举值的方法?

How to type-hint a method that retrieves dynamic enum value?

我有一个 Python 模块,其中有许多定义如下的简单枚举:

class WordType(Enum):
    ADJ = "Adjective"
    ADV = "Adverb"

class Number(Enum):
    S = "Singular"
    P = "Plural"

因为有很多这样的枚举,而且我只在运行时决定查询哪些枚举来查询任何给定的输入,所以我想要一个可以检索给定枚举类型和枚举值作为字符串的值的函数。我成功地做到了,如下所示:

names = inspect.getmembers(sys.modules[__name__], inspect.isclass)

def get_enum_type(name: str):
    enum_class = [x[1] for x in names if x[0] == name]
    return enum_class[0]

def get_enum_value(object_name: str, value_name: str):
    return get_enum_type(object_name)[value_name]

这很好用,但现在我正在添加类型提示,我正在为如何为这些方法定义 return 类型而苦苦挣扎:我试过 sliceLiteral[],两者都由 mypy 建议,但都没有签出(可能是因为我不明白我可以给 Literal[] 什么类型的参数)。

我愿意修改枚举定义,但我更愿意保持动态查询不变。最坏的情况,我可以做 # type: ignore 或只是 return -> Any,但我希望有更好的东西。

您始终可以 运行 您的函数并打印函数的结果以了解它应该是什么。请注意,您可以像任何其他 class.

一样在类型提示中使用 Enum

例如:

>>> result = get_enum_type('WordType')
... print(result)
... print(type(result))
<enum 'WordType'>
<class 'enum.EnumMeta'>

所以你可以实际使用

get_enum_type(name: str) -> EnumMeta

但是您可以使用 typing 中的 Type 使其更漂亮,因为 EnumMeta 是通用枚举的类型。

get_enum_type(name: str) -> Type[Enum]

对于 get_enum_value 的类似过程,您会得到

>>> type(get_enum_value('WordType', 'ADJ'))
<enum 'WordType'>

显然你不会总是 return 类型 WordType 所以你可以使用 Enum 概括 return 类型。

总结一下:

get_enum_type(name: str) -> Type[Enum]
get_enum_value(object_name: str, value_name: str) -> Enum

因为你不想检查 any Enum 的类型,我建议引入一个基本类型(比如 GrammaticalEnum)来标记您所有的 Enum 并将它们分组到一个自己的模块中:

# module grammar_enums
import sys
import inspect
from enum import Enum


class GrammaticalEnum(Enum):
    """use as a base to mark all grammatical enums"""
    pass


class WordType(GrammaticalEnum):
    ADJ = "Adjective"
    ADV = "Adverb"


class Number(GrammaticalEnum):
    S = "Singular"
    P = "Plural"


# keep this statement at the end, as all enums must be known first
grammatical_enums = dict(
    m for m in inspect.getmembers(sys.modules[__name__], inspect.isclass)
    if issubclass(m[1], GrammaticalEnum))

# you might prefer the shorter alternative:
# grammatical_enums = {k: v for (k, v) in globals().items()
#                      if inspect.isclass(v) and issubclass(v, GrammaticalEnum)}

关于打字,yakir0 已经建议了正确的类型,
但是有了共同的基础,你可以缩小它们的范围。

如果你愿意,你甚至可以完全摆脱你的功能:

from grammar_enums import grammatical_enums as g_enums
from grammar_enums import GrammaticalEnum

# just use g_enums instead of get_enum_value like this
WordType_ADJ: GrammaticalEnum = g_enums['WordType']['ADJ']

# ...or use your old functions:

# as your grammatical enums are collected in a dict now,
# you don't need this function any more:
def get_enum_type(name: str) -> Type[GrammaticalEnum]:
    return g_enums[name]


def get_enum_value(enum_name: str, value_name: str) -> GrammaticalEnum:
    # return get_enum_type(enum_name)[value_name]
    return g_enums[enum_name][value_name]

正如我在评论中所说,我认为不可能拥有您的动态代码并让 MyPy 预测输出。例如,我认为 MyPy 不可能知道 get_enum_type("WordType") 应该是 WordTypeget_enum_type("Number") 应该是 Number.

正如其他人所说,您可以澄清它们将是枚举。您可以添加一个基本类型,并说它们将专门成为其中一种基本类型。部分问题在于,尽管您可以承诺,但 MyPy 无法确认。它不知道 inspect.getmembers(sys.modules[__name__], inspect.isclass)[1].

中只会有 EnumsGrammaticalEnum

如果您愿意更改查找的实现,那么我建议您可以利用 __init_subclass__ 获利。像

GRAMMATICAL_ENUM_LOOKUP: "Mapping[str, GrammaticalEnum]" = {}
class GrammaticalEnum(Enum):
    def __init_subclass__(cls, **kwargs):
        GRAMMATICAL_ENUM_LOOKUP[cls.__name__] = cls
        super().__init_subclass__(**kwargs)

 def get_enum_type(name: str) -> Type[GrammaticalEnum]:
     return GRAMMATICAL_ENUM_LOOKUP[name]

这至少有一个好处,即 MyPy 可以看到正在发生的事情,并且应该对此感到普遍满意。它知道所有内容实际上都是有效的 GrammaticalEnum,因为这就是 GRAMMATICAL_ENUM_LOOKUP 填充的所有内容。