Dan*_*erg 2 python generics base-class python-typing
假设我们要定义一个继承自 的自定义通用(基)类typing.Generic
。
为了简单起见,我们希望它由单个类型变量 T
来参数化。所以类的定义是这样开始的:
from typing import Generic, TypeVar
T = TypeVar("T")
class GenericBase(Generic[T]):
...
Run Code Online (Sandbox Code Playgroud)
T
有没有办法访问 的任何特定子类中的类型参数GenericBase
?
该解决方案应该足够通用,能够在具有附加基础的子类中工作GenericBase
,并且独立于实例化(即在类级别上工作)。
期望的结果是这样的类方法:
class GenericBase(Generic[T]):
@classmethod
def get_type_arg(cls) -> Type[T]:
...
Run Code Online (Sandbox Code Playgroud)
class Foo:
pass
class Bar:
pass
class Specific(Foo, GenericBase[str], Bar):
pass
print(Specific.get_type_arg())
Run Code Online (Sandbox Code Playgroud)
输出应该是<class 'str'>
.
如果所有相关的类型注释都已完成,以便静态类型检查器可以正确推断get_type_arg
.
GenericBase
从子类的元组中获取__orig_bases__
,将其传递给typing.get_args
,从它返回的元组中获取第一个元素,并确保您拥有的是具体类型。
get_args
正如这篇文章所指出的,typing
Python 的模块3.8+
提供了该get_args
功能。它很方便,因为给定泛型类型的特化get_args
,返回其类型参数(作为元组)。
示范:
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
print(get_args(GenericBase[int]))
Run Code Online (Sandbox Code Playgroud)
输出:
(<class 'int'>,)
Run Code Online (Sandbox Code Playgroud)
这意味着一旦我们能够访问专用 GenericBase
类型,我们就可以轻松提取其类型参数。
__orig_bases__
正如前面提到的帖子中进一步指出的,有一个方便的小类属性,它是在创建新类时__orig_bases__
由元类设置的。它在 中type
提到过,但在其他方面几乎没有记录。PEP 560
该属性包含(顾名思义)原始基数,因为它们以元组的形式传递给元类构造函数。这与 不同__bases__
,后者包含由 所返回的已解析types.resolve_bases
碱基。
示范:
(<class 'int'>,)
Run Code Online (Sandbox Code Playgroud)
输出:
(<class '__main__.GenericBase'>,)
(__main__.GenericBase[int],)
Run Code Online (Sandbox Code Playgroud)
我们对原始基类感兴趣,因为这是我们泛型类的特化,这意味着它是“知道”类型参数的类(int
在本例中),而解析后的基类只是type
.
如果我们将这两者放在一起,我们可以快速构建一个简单的解决方案,如下所示:
from typing import Generic, TypeVar
T = TypeVar("T")
class GenericBase(Generic[T]):
pass
class Specific(GenericBase[int]):
pass
print(Specific.__bases__)
print(Specific.__orig_bases__)
Run Code Online (Sandbox Code Playgroud)
输出:
<class 'int'>
Run Code Online (Sandbox Code Playgroud)
但是,一旦我们在GenericBase
.
(<class '__main__.GenericBase'>,)
(__main__.GenericBase[int],)
Run Code Online (Sandbox Code Playgroud)
输出:
Traceback (most recent call last):
...
return get_args(cls.__orig_bases__[0])[0]
IndexError: tuple index out of range
Run Code Online (Sandbox Code Playgroud)
发生这种情况是因为cls.__orig_bases__[0]
now 恰好Mixin
不是参数化类型,因此get_args
返回一个空元组()
。
所以我们需要的是一种从元组中明确识别 的方法。GenericBase
__orig_bases__
get_origin
就像为typing.get_args
我们提供泛型类型的类型参数一样,typing.get_origin
为我们提供泛型类型的未指定版本。
示范:
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
@classmethod
def get_type_arg_simple(cls):
return get_args(cls.__orig_bases__[0])[0]
class Specific(GenericBase[int]):
pass
print(Specific.get_type_arg_simple())
Run Code Online (Sandbox Code Playgroud)
输出:
<class '__main__.GenericBase'>
True
Run Code Online (Sandbox Code Playgroud)
有了这些组件,我们现在可以编写一个函数get_type_arg
,它接受一个类作为参数,并且(如果该类是我们的特殊形式GenericBase
)返回其类型参数:
<class 'int'>
Run Code Online (Sandbox Code Playgroud)
输出:
<class 'int'>
Run Code Online (Sandbox Code Playgroud)
现在剩下要做的就是直接将其嵌入为 的类方法GenericBase
,稍微优化一下并修复类型注释。
我们可以做的一件事是对此进行优化,即对于 的任何给定子类(即定义它时)仅运行该算法一次GenericBase
,然后将该类型保存在类属性中。由于特定类的类型参数可能永远不会改变,因此每次我们想要访问类型参数时都不需要计算它。为了实现这一点,我们可以挂入__init_subclass__
并在那里执行我们的循环。
get_type_arg
我们还应该为(未指定的)泛型类调用时定义正确的响应。安AttributeError
似乎合适。
from typing import Generic, TypeVar, get_args
T = TypeVar("T")
class GenericBase(Generic[T]):
@classmethod
def get_type_arg_simple(cls):
return get_args(cls.__orig_bases__[0])[0]
class Mixin:
pass
class Specific(Mixin, GenericBase[int]):
pass
print(Specific.get_type_arg_simple())
Run Code Online (Sandbox Code Playgroud)
输出:
<class 'int'>
<class 'str'>
Run Code Online (Sandbox Code Playgroud)
像 PyCharm 这样的 IDE 甚至可以为返回的任何类型提供正确的自动建议get_type_arg
,这非常好。
__orig_bases__
属性没有详细记录。我不确定它是否应该被认为是完全稳定的。尽管它似乎也不是“只是一个实现细节”。我建议密切关注这一点。mypy
似乎同意这一警告,并no attribute
在您访问的地方引发错误__orig_bases__
。因此 atype: ignore
被放置在该行中。GenericBase
GenericBase[str].get_type_arg()
typing.get_args
归档时间: |
|
查看次数: |
1128 次 |
最近记录: |