文字选项的编程生成

Programmatic generation of Literal options

MyPy 的文字类型对于定义可用选项非常有用。是否可以通过编程方式生成文字类型,例如来自规范注册表?

例如

class Dispatcher():
    func_reg = {
        'f1': my_func,
        'f2': new_func,
        'f3': shoe_func,
    }

    def dispatch(cls, func_name: Literal[*func_reg.keys()]) -> Whatever:
        pass

很遗憾,答案是否定的。

根据 mypy documentation:

Literal types may contain one or more literal bools, ints, strs, bytes, and enum values. However, literal types cannot contain arbitrary expressions: types like Literal[my_string.trim()], Literal[x > 3], or Literal[3j + 4] are all illegal.

正如@BrokenBenchmark 指出的那样,auto-generate Literal 类型是不可能的。但是,如果最终目标只是要求从某种函数注册表生成特定值,我们可以使用 enum.Enum.

进行破解。

引用PEP 586

rather than entirely special-casing enums, we can instead treat them as being approximately equivalent to the union of their values... the Status enum could be treated as being approximately equivalent to Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]

在这里,函数是通过向 FuncNames 枚举添加一个枚举值来“注册”的,该枚举值是函数名称的精确大写。这不是一个漂亮或健壮的解决方案,但它可以运行,它支持 single-location 为 type-checked 调度注册一个函数,并且 mypy 按预期处理所需的枚举值。

from enum import Enum, auto

def f():
    return "f"

def g():
    return "g"

def h():
    return "h"

class Dispatcher():
    # Build the enum used to register the functions
    class FuncNames(Enum):
        """
        The enum names here _must_ be exact uppercase-ings of the function
        names. The names will be lowercased and evaluated to register their
        associated functions
        """
        F = auto()
        G = auto()
        H = auto()


    # NOTE: The functional syntax works just as well
    # FuncNames = Enum('FuncNames', 'F G H')


    # Comprehensions can't access names defined in the class block,
    # so use a standard for loop
    func_reg = dict()
    for name in list(FuncNames):
        func_reg[eval(f"FuncNames.{name.name}")] = eval(str(name.name).lower())


    @classmethod
    def dispatch(cls, func_name: FuncNames):
        """
        Prints the return from a registered function.
        Can only be called with an item from FuncNames
        """
        print(cls.func_reg[func_name]())

Dispatcher.dispatch(Dispatcher.FuncNames.F)
Dispatcher.dispatch(Dispatcher.FuncNames.G)
Dispatcher.dispatch(Dispatcher.FuncNames.H)
# Dispatcher.dispatch(Dispatcher.FuncNames.I) -> "FuncNames has no attribute I"
# Dispatcher.dispatch(Dispatcher2.FuncNames) -> "incompatible type"
# Dispatcher.dispatch('MyPy hates me!') -> "incompatible type"

有趣的是,虽然从函数本身的列表生成枚举本身感觉更干净,但 MyPy 对此感到窒息。

class Dispatcher2():
    # Build an enum used to register these (the actual functions)
    funcs_to_register = [f, g, h]
    enum_names = [func.__name__.upper() for func in funcs_to_register]
    joined = ' '.join(enum_names)
    FuncNames = Enum('FuncNames', joined)

    func_reg = dict()
    for name in enum_names:
        func_reg[eval(f"FuncNames.{name}")] = eval(name.lower())


    @classmethod
    def dispatch(cls, func_name: FuncNames):
        """
        Prints the return from a registered function.
        Can only be called with an item from FuncNames
        """
        print(cls.func_reg[func_name]())

Dispatcher2.dispatch(Dispatcher2.FuncNames.F)
Dispatcher2.dispatch(Dispatcher2.FuncNames.G)
Dispatcher2.dispatch(Dispatcher2.FuncNames.H)

以上按预期运行,但 mypy 可能无法推断枚举中存在的值,除非它是静态定义的,因此它会出错。

> mypy enums_typing.py 
enums_typing.py:19: error: Enum() expects a string, tuple, list or dict literal as the second argument
enums_typing.py:36: error: "Type[FuncNames]" has no attribute "F"
enums_typing.py:37: error: "Type[FuncNames]" has no attribute "G"
enums_typing.py:38: error: "Type[FuncNames]" has no attribute "H"
Found 4 errors in 1 file (checked 1 source file)

TLDR: 为了定义 MyPy 可以检查的一组固定选项,您必须静态定义它们。然后可以使用这些 statically-defined 选择以编程方式构建函数注册表。