在运行时获取任意高泛型父类的类型参数

Mar*_*hac 7 python type-hinting mypy python-typing

鉴于这种:

from typing import Generic, TypeVar

T = TypeVar('T')

class Parent(Generic[T]):
    pass
Run Code Online (Sandbox Code Playgroud)

我可以intParent[int]使用中得到typing.get_args(Parent[int])[0]

问题变得有点复杂,如下所示:

class Child1(Parent[int]):
    pass

class Child2(Child1):
    pass
Run Code Online (Sandbox Code Playgroud)

为了支持任意长的继承层次结构,我提出了以下解决方案:

import typing
from dataclasses import dataclass

@dataclass(frozen=True)
class Found:
    value: Any

def get_parent_type_parameter(child: type) -> Optional[Found]:
    for base in child.mro():
        # If no base classes of `base` are generic, then `__orig_bases__` is nonexistent causing an `AttributeError`.
        # Instead, we want to skip iteration.
        for generic_base in getattr(base, "__orig_bases__", ()):
            if typing.get_origin(generic_base) is Parent:
                [type_argument] = typing.get_args(generic_base)

                # Return `Found(type_argument)` instead of `type_argument` to differentiate between `Parent[None]` 
                # as a base class and `Parent` not appearing as a base class.
                return Found(type_argument)

    return None
Run Code Online (Sandbox Code Playgroud)

这样get_parent_type_parameter(Child2)返回int. 我只对一个特定基类 ( ) 的类型参数感兴趣Parent,因此我将该类硬编码到其中get_parent_type_parameter并忽略任何其他基类。

但我的上述解决方案因这样的链而崩溃:

class Child3(Parent[T], Generic[T]):
    pass
Run Code Online (Sandbox Code Playgroud)

其中get_parent_type_parameter(Child3[int])返回T而不是int.

虽然任何解决问题的答案Child3都已经很好了,但能够处理这样的情况Child4会更好:

from typing import Sequence

class Child4(Parent[Sequence[T]], Generic[T]):
    pass
Run Code Online (Sandbox Code Playgroud)

所以get_parent_type_parameter(Child4[int])返回Sequence[int]

是否有一种更可靠的方法可以在运行时访问类的类型参数(X给定注释Awhere issubclass(typing.get_origin(A), X)is )True

为什么我需要这个:

最近的 Python HTTP 框架根据函数的带注释的返回类型生成端点文档(和响应架构)。例如:

app = ...

@dataclass
class Data:
    hello: str

@app.get("/")
def hello() -> Data:
    return Data(hello="world")
Run Code Online (Sandbox Code Playgroud)

我正在尝试扩展它以解释状态代码和其他非正文组件:

@dataclass
class Error:
    detail: str

class ClientResponse(Generic[T]):
    status_code: ClassVar[int]
    body: T

class OkResponse(ClientResponse[Data]):
    status_code: ClassVar[int] = 200

class BadResponse(ClientResponse[Error]):
    status_code: ClassVar[int] = 400

@app.get("/")
def hello() -> Union[OkResponse, BadResponse]:
    if random.randint(1, 2) == 1:
        return OkResponse(Data(hello="world"))

    return BadResponse(Error(detail="a_custom_error_label"))
Run Code Online (Sandbox Code Playgroud)

为了生成OpenAPI文档,我的框架将在检查传递给 的函数的带注释的返回类型后对每个框架进行评估get_parent_type_parameter(E)(硬编码为中ClientResponse的父级get_parent_type_parameter)。所以首先会导致. 那么就会这样,导致。然后,我的框架会迭代每种类型,并在客户端的文档中生成响应模式。EUnionapp.getEOkResponseDataErrorResponseError__annotations__body

a_g*_*est 6

以下方法基于__class_getitem____init_subclass__。它可能适合您的用例,但它有一些严重的限制(见下文),因此请根据您自己的判断使用。

from __future__ import annotations

from typing import Generic, Sequence, TypeVar


T = TypeVar('T')


NO_ARG = object()


class Parent(Generic[T]):
    arg = NO_ARG  # using `arg` to store the current type argument

    def __class_getitem__(cls, key):
        if cls.arg is NO_ARG or cls.arg is T:
            cls.arg = key 
        else:
            try:
                cls.arg = cls.arg[key]
            except TypeError:
                cls.arg = key
        return super().__class_getitem__(key)

    def __init_subclass__(cls):
        if Parent.arg is not NO_ARG:
            cls.arg, Parent.arg = Parent.arg, NO_ARG


class Child1(Parent[int]):
    pass


class Child2(Child1):
    pass


class Child3(Parent[T], Generic[T]):
    pass


class Child4(Parent[Sequence[T]], Generic[T]):
    pass


def get_parent_type_parameter(cls):
    return cls.arg


classes = [
    Parent[str],
    Child1,
    Child2,
    Child3[int],
    Child4[float],
]
for cls in classes:
    print(cls, get_parent_type_parameter(cls))

Run Code Online (Sandbox Code Playgroud)

其输出如下:

__main__.Parent[str] <class 'str'>
<class '__main__.Child1'> <class 'int'>
<class '__main__.Child2'> <class 'int'>
__main__.Child3[int] <class 'int'>
__main__.Child4[float] typing.Sequence[float]
Run Code Online (Sandbox Code Playgroud)

此方法要求 every Parent[...](ie __class_getitem__) 后跟一个__init_subclass__,否则前一个信息可能会被第二个覆盖Parent[...]。因此,它不适用于类型别名。考虑以下:

__main__.Parent[str] <class 'str'>
<class '__main__.Child1'> <class 'int'>
<class '__main__.Child2'> <class 'int'>
__main__.Child3[int] <class 'int'>
__main__.Child4[float] typing.Sequence[float]
Run Code Online (Sandbox Code Playgroud)

其输出:

__main__.Parent[str] <class 'float'>
__main__.Parent[int] <class 'float'>
__main__.Parent[float] <class 'float'>
Run Code Online (Sandbox Code Playgroud)