访问用户定义的 Generic[T] 类的任何特定子类中的类型参数

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.

相关问题

Dan*_*erg 6

长话短说

GenericBase从子类的元组中获取__orig_bases__,将其传递给typing.get_args,从它返回的元组中获取第一个元素,并确保您拥有的是具体类型。

1)开始于get_args

正如这篇文章所指出的,typingPython 的模块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类型,我们就可以轻松提取其类型参数。

2) 继续__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.

3)简单的解决方案

如果我们将这两者放在一起,我们可以快速构建一个简单的解决方案,如下所示:

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__

4)认同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)

5)将它们放在一起

有了这些组件,我们现在可以编写一个函数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似乎合适。

6) 完整的工作示例

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,这非常好。

7) 注意事项

  • __orig_bases__属性没有详细记录。我不确定它是否应该被认为是完全稳定的。尽管它似乎也不是“只是一个实现细节”。我建议密切关注这一点。
  • mypy似乎同意这一警告,并no attribute在您访问的地方引发错误__orig_bases__。因此 atype: ignore被放置在该行中。
  • 整个设置是针对我们的泛型类的一个单一类型参数。它可以相对容易地适应多个参数,尽管类型检查器的注释可能会变得更加棘手。
  • 当直接从专门的类(即 )调用时,此方法不起作用。但为此,我们只需要像一开始所示那样调用它即可。GenericBaseGenericBase[str].get_type_arg()typing.get_args