【问题标题】:How can I create my own "parameterized" type in Python (like `Optional[T]`)?如何在 Python 中创建自己的“参数化”类型(如 `Optional[T]`)?
【发布时间】:2023-04-03 12:47:02
【问题描述】:

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

class MaybeWrapped:
    # magic goes here

T = TypeVar('T')

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

别介意人为的例子;我该如何实施?我查看了 Union 和 Optional 的源代码,但它看起来像是一些我想避免的相当低级的黑客行为。

文档中唯一的建议来自example re-implementation of Mapping[KT,VT] that inherits from Generic。但这个例子更多的是关于 __getitem__ 方法而不是类本身。

【问题讨论】:

    标签: python python-3.x generics metaclass type-hinting


    【解决方案1】:

    如果您只是想创建通用类或函数,请尝试查看documentation on mypy-lang.org about generic types——它相当全面,比标准库类型文档更详细。

    如果您尝试实现您的具体示例,值得指出的是 type aliases work with 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 module repo 中打开问题来完成此操作。

    【讨论】:

    • 谢谢,使用TypeVar 这是我所缺少的。泛型像这样“正常工作”真是太神奇了。
    • 出于好奇,您是否知道“构造某种符合 PEP 484 的类型检查器可以理解和使用的自定义类/元类的东西”选项现在是否可行?
    【解决方案2】:

    正是__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
    

    在交互模式下尝试这个,可以做到:

    In [27]: Base[int]
    Out[27]: types.Base_int
    

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

    【讨论】:

    • 谢谢,虽然我仍然不确定这一切是如何运作的。你能谈谈我问题中的例子吗?您回答中的示例似乎与我尝试做的有点不同。
    • @jsbueno -- 我不认为你的答案有效。您提出的代码当然是一种构建看起来像 PEP 484 类型的东西的方法,但由于它不是,因此符合 PEP 484 的类型检查器将无法理解如何处理您的 MyMetaBase 类。
    • 哇!伟大的!你能解释一下lambda ns: ns.__setitem__("type", key)吗?如果我好了ns 是所谓的“命名空间”,但在这种情况下是什么意思以及在哪里可以找到提供的键“类型”?
    • 知道了!它只是类的 dict 本身,因此可以通过类或实例的属性访问。 b=Base[int]assert b.type is int
    【解决方案3】:

    我想根据@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
    

    【讨论】:

    • 顺便说一句,现在我们有了__class_getitem__(cls, item),在基类中定义它,我们可以在没有元类的情况下做同样的事情。
    猜你喜欢
    • 1970-01-01
    • 2019-12-14
    • 1970-01-01
    • 2017-02-20
    • 1970-01-01
    • 2015-08-15
    • 2014-07-13
    • 2012-08-23
    • 2020-09-22
    相关资源
    最近更新 更多