如何在Python中创建自己的“参数化”类型(如“ Optional [T]”)?

sha*_*ker 5 python generics metaclass type-hinting python-3.x

我想在Python中创建自己的参数化类型以用于类型提示:

class MaybeWrapped:
    # magic goes here

T = TypeVar('T')

assert MaybeWrapped[T] == Union[T, Tuple[T]]
Run Code Online (Sandbox Code Playgroud)

不用管这个人为的例子。我该如何实现呢?我查看了Union和Optional的来源,但看起来我想避免一些相当底层的黑客。

该文档中的唯一建议来自继承自Generic示例的重新实现Mapping[KT,VT]。但是那个例子更多的是关于__getitem__方法的,而不是关于类本身的。

jsb*_*eno 9

这正是__getitem__发挥所有魔力的方法。

[这是当您用和括号订阅一个名称时调用的方法]

因此,您 __getitem__的类的类中需要一个方法 - 即它的元类,它将获取括号内的任何内容作为参数。该方法负责动态创建(或检索缓存的副本)您想要生成的任何内容,然后返回它。

我只是无法想象您希望如何进行类型提示,因为打字库似乎涵盖了所有合理的情况(我想不出他们还没有涵盖的示例)。但是,假设您希望一个类返回其自身的副本,但将参数注释为其type_属性:

class MyMeta(type):
    def __getitem__(cls, key):
        new_cls = types.new_class(f"{cls.__name__}_{key.__name__}", (cls,), {}, lambda ns: ns.__setitem__("type", key))
        return new_cls

class Base(metaclass=MyMeta): pass
Run Code Online (Sandbox Code Playgroud)

在交互模式下尝试这一点时,可以这样做:

In [27]: Base[int]
Out[27]: types.Base_int
Run Code Online (Sandbox Code Playgroud)

更新:从Python 3.7开始,还有一个专门__class_getitem__为此目的而创建的特殊方法:它充当类方法,并且避免了仅针对这种情况的元类的需要。a 中写的任何内容都metaclass.__getitem__可以直接放入cls.__class_getitem__方法中。PEP 560中定义


Mic*_*x2a 7

如果您只是想创建泛型类或函数,请尝试查看mypy-lang.org 上有关泛型类型文档——它相当全面,而且比标准库类型文档更详细。

如果您正在尝试实现您的特定示例,则值得指出类型别名与类型变量一起使用——您可以简单地执行以下操作:

from typing import Union, TypeVar, Tuple

T = TypeVar('T')

MaybeWrapped = Union[T, Tuple[T]]

def foo(x: int) -> MaybeWrapped[str]:
    if x % 2 == 0:
        return "hi"
    else:
        return ("bye",)

# When running mypy, the output of this line is:
# test.py:13: error: Revealed type is 'Union[builtins.str, Tuple[builtins.str]]'
reveal_type(foo(3))
Run Code Online (Sandbox Code Playgroud)

但是,如果您尝试构建具有真正新语义的泛型类型,则很可能不走运。您剩下的选择是:

  1. 构建某种符合 PEP 484 的类型检查器可以理解和使用的自定义类/元类事物。
  2. 以某种方式修改您正在使用的类型检查器(例如,mypy 有一个实验性的“插件”系统)
  3. 请愿修改 PEP 484 以包含您的新自定义类型(您可以通过在输入模块 repo 中打开一个问题来实现此目的)。


Ant*_*kov 5

我想根据@jsbueno 的回答提出改进的解决方案。现在我们的“泛型”可以用于比较和身份检查,并且它们在键入时表现得像“真正的”泛型。我们还可以禁止非类型化类本身的实例化。而且!我们isinstance免费检查!

还满足BaseMetaMixin完美静态类型检查的类!

import types
from typing import Type, Optional, TypeVar, Union

T = TypeVar('T')


class BaseMetaMixin:
    type: Type


class BaseMeta(type):
    cache = {}

    def __getitem__(cls: T, key: Type) -> Union[T, Type[BaseMetaMixin]]:
        if key not in BaseMeta.cache:
            BaseMeta.cache[key] = types.new_class(
                f"{cls.__name__}_{key.__name__}",
                (cls,),
                {},
                lambda ns: ns.__setitem__("type", key)
            )

        return BaseMeta.cache[key]

    def __call__(cls, *args, **kwargs):
        assert getattr(cls, 'type', None) is not None, "Can not instantiate Base[] generic"
        return super().__call__(*args, **kwargs)


class Base(metaclass=BaseMeta):
    def __init__(self, some: int):
        self.some = some


# identity checking
assert Base[int] is Base[int]
assert Base[int] == Base[int]
assert Base[int].type is int
assert Optional[int] is Optional[int]

# instantiation
# noinspection PyCallByClass
b = Base[int](some=1)
assert b.type is int
assert b.some == 1

try:
    b = Base(1)
except AssertionError as e:
    assert str(e) == 'Can not instantiate Base[] generic'

# isinstance checking
assert isinstance(b, Base)
assert isinstance(b, Base[int])
assert not isinstance(b, Base[float])

exit(0)
# type hinting in IDE
assert b.type2 is not None # Cannot find reference 'type2' in 'Base | BaseMetaMixin'
b2 = Base[2]()  # Expected type 'type', got 'int' instead
Run Code Online (Sandbox Code Playgroud)