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

24
我想在Python中创建自己的参数化类型,以便在类型提示中使用:
class MaybeWrapped:
    # magic goes here

T = TypeVar('T')

assert MaybeWrapped[T] == Union[T, Tuple[T]]

忽略那个刻意构造的例子; 我该如何实现这个? 我查看了Union和Optional的源代码,但它看起来像是一些相当底层的黑科技,我想避免使用。文档中唯一的建议来自一个继承自Generic的KT,VT映射重写示例。但那个示例更多关注于__getitem__方法而不是类本身。
3个回答

21
如果您只是想创建通用的类或函数,请查看关于泛型类型的文档,可以访问mypy-lang.org,该文档相当全面,比标准库的类型文档更详细。
如果您正在尝试实现特定的示例,值得指出的是类型别名与 typevars 配合使用 -- 您可以简单地将其做如下操作:
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))

然而,如果您试图构建具有全新语义的通用类型,则很可能会失败。您的剩余选项是:

  1. 构建一种自定义类/元类,符合PEP 484兼容的类型检查器可以理解和使用。
  2. 以某种方式修改您正在使用的类型检查器(例如,mypy具有实验性的“插件”系统)
  3. 请愿修改PEP 484以包括您的新自定义类型(您可以通过在typing模块存储库中打开一个问题来完成此操作)。

1
谢谢,使用TypeVar就是我所缺少的。泛型像这样“只是工作”真是太神奇了。 - shadowtalker
1
出于好奇,您是否知道现在是否有可能“构建某种自定义类/元类,以便PEP 484兼容的类型检查器可以理解和使用该选项”? - shadowtalker

9

正是 __getitem__ 方法完成了所有的魔法。

当您使用 [] 括号订阅一个名称时,就会调用该方法。

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

我真的无法想象您希望这样做的原因是什么,因为 typing 库似乎涵盖了所有合理的情况(我无法想到他们尚未涵盖的例子)。但是让我们假设您想要一个类返回其自身的副本,但参数注释为其 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

在交互模式下尝试时,可以执行以下操作:

In [27]: Base[int]
Out[27]: types.Base_int
更新:自Python 3.7起,还有一种特殊方法__class_getitem__专门为此而创建:它充当类方法,并避免了需要元类的情况。任何在metaclass.__getitem__中编写的内容都可以直接放入cls.__class_getitem__方法中。定义在PEP 560中。

谢谢,虽然我仍然不确定这一切是如何工作的。你能解释一下我问题中的例子吗?你回答中的例子似乎和我尝试做的有些不同。 - shadowtalker
1
@jsbueno -- 我认为你的答案不可行。你提出的代码当然是构建类似于 PEP 484 类型的一种方式,但由于它不是 PEP 484 兼容的类型检查器,无法理解如何处理你的 MyMetaBase 类。 - Michael0x2a
哇!太棒了!你能解释一下 lambda ns: ns.__setitem__("type", key) 吗?如果我没记错的话,ns 是所谓的“命名空间”,但在这个上下文中它的意义是什么?提供的关键字“type”在哪里可以找到? - Anton Ovsyannikov
1
明白了!它只是类的 dict 本身,因此可以通过类或实例的属性进行访问。b=Base[int] assert b.type is int - Anton Ovsyannikov
作为对“typing库似乎涵盖了所有合理的情况”的回应:在您参考PEP 560时,它说:“所有通用类型都是GenericMeta的实例,因此如果用户使用自定义元类,则很难使相应的类成为通用的。” 我恰好就有这种情况。 - Jonathan Komar

4
我想提出一种改进的解决方案,基于@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

1
顺便提一下,现在我们有__class_getitem__(cls, item),在基类中定义它,我们可以不使用元类来完成相同的操作。 - Anton Ovsyannikov
这是一个非常棒的想法。你可以通过使用dict.setdefault来简化你的__getitem__逻辑。 - ringo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接