如何在 Python 中创建自己的“参数化”类型(如 `Optional[T]`)?
Posted
技术标签:
【中文标题】如何在 Python 中创建自己的“参数化”类型(如 `Optional[T]`)?【英文标题】:How can I create my own "parameterized" type in Python (like `Optional[T]`)? 【发布时间】:2018-03-05 01:31:15 【问题描述】:我想在 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__
方法而不是类本身。
【问题讨论】:
【参考方案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))
但是,如果您尝试构建具有真正新语义的泛型类型,那么您很可能不走运。您剩下的选择是:
-
构建符合 PEP 484 的类型检查器可以理解和使用的某种自定义类/元类。
以某种方式修改您正在使用的类型检查器(例如,mypy 有一个实验性“插件”系统)
请求修改 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 的类型检查器将无法理解如何处理您的MyMeta
或Base
类。
哇!伟大的!你能解释一下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)
,在基类中定义它,我们可以在没有元类的情况下做同样的事情。以上是关于如何在 Python 中创建自己的“参数化”类型(如 `Optional[T]`)?的主要内容,如果未能解决你的问题,请参考以下文章
为什么在外部参数化类中创建一个数组,其中组件类型是一个被认为是“通用数组创建”的内部类?
(如何)在 boost 几何中创建自己的多边形类型并使用 multi_polygon 类型?