在运行时获取任意高的通用父级 Class 的类型参数

Get Type Argument of Arbitrarily High Generic Parent Class at Runtime

鉴于此:

from typing import Generic, TypeVar

T = TypeVar('T')

class Parent(Generic[T]):
    pass

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

下面的问题变得有点复杂:

class Child1(Parent[int]):
    pass

class Child2(Child1):
    pass

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

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

这样 get_parent_type_parameter(Child2) returns int。我只对一个特定基数 class (Parent) 的类型参数感兴趣,所以我将 class 硬编码为 get_parent_type_parameter 并忽略任何其他基数 classes.

但是我上面的解决方案被这样的链破坏了:

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

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

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

from typing import Sequence

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

所以get_parent_type_parameter(Child4[int])returnsSequence[int].

是否有更可靠的方法在运行时访问 class X 的类型参数给定注释 A 其中 issubclass(typing.get_origin(A), X)True?

为什么我需要这个:

最近的 Python HTTP 框架从函数的注释 return 类型生成端点文档(和响应模式)。例如:

app = ...

@dataclass
class Data:
    hello: str

@app.get("/")
def hello() -> Data:
    return Data(hello="world")

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

@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"))

为了生成 OpenAPI 文档,我的框架将在每个 E 上评估 get_parent_type_parameter(E)ClientResponse 被硬编码为 get_parent_type_parameter 中的父级) Union 在检查传递给 app.get 的函数的注释 return 类型之后。所以 E 将是 OkResponse 首先导致 Data。那么它将是 ErrorResponse,导致 Error。然后,我的框架遍历每个 body 类型的 __annotations__ 并在客户端文档中生成响应模式。

以下方法基于__class_getitem__ and __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))

输出如下:

__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]

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

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

输出:

__main__.Parent[str] <class 'float'>
__main__.Parent[int] <class 'float'>
__main__.Parent[float] <class 'float'>

编辑:以下解决方案实现了一个名为 infer_type_args 的函数,它硬编码了标准泛型类型的默认类型参数(collectionscollections.abc 中的任何内容或采用类型参数的内置函数) 并以其他方式递归通过对象的基本类型来推断类型参数。此外,ParamSpecs 也使用此解决方案进行处理。然而,该解决方案增加了很多复杂性,那些不需要积极概括的人应该查看 Cosmo 的解决方案,它适用于大多数情况。

from __future__ import annotations

import typing
from collections import *
from collections.abc import *
from contextlib import AbstractAsyncContextManager, AbstractContextManager
from dataclasses import dataclass
from types import EllipsisType, GenericAlias, NoneType
from typing import _BaseGenericAlias, Any, Concatenate, Final, Generic, List, ParamSpec, TypeVar


# These type variables will be used to given default generic inheritance for standard library generic types.
T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
P = ParamSpec('P')
TP = TypeVar('TP')

# Type of a generic alias which includes both user-defined and standard library aliases.
GeneralAlias = _BaseGenericAlias | GenericAlias

# Type of a generic alias which has accepted type arguments, even if they are type variables.
ParameterizedAlias = type(List[int]) | GenericAlias

# Type of an entity which is either a type or a type alias.
TypeOrAlias = type[object] | GeneralAlias

# Any kind of type parameter, including TupleParam.
TypeVariable = TypeVar | ParamSpec
TypeVariables = Sequence[TypeVariable, ...]

# Type of a generic type. Of course, it includes types that are not generic.
GenericType = type[
    type |
    AbstractAsyncContextManager |
    AbstractContextManager |
    AsyncIterable |
    Awaitable |
    Callable |
    Container |
    Generic |
    Iterable
]

# Type of an entity that can (sanely) be used as a type argument for a TypeVar.
TypeVarArg = type[object] | GeneralAlias | TypeVar | type(Any)

# Type of an entity that can (sanely) be used as a type argument for ParamSpec.
ParamSpecArg = tuple[TypeVarArg, ...] | list[TypeVarArg] | EllipsisType | ParamSpec | type(Concatenate)

# Type of an entity that represents sane type arguments to supply to a tuple.
TupleParamArg = tuple[TypeVarArg, ...] | tuple[TypeVarArg, EllipsisType]

# Type of any type parameter argument.
TypeArg = TypeVarArg | ParamSpecArg | TupleParamArg
TypeArgs = tuple[TypeArg, ...]

# Type of a type parameter argument which has been processed (see the process function below).
ProcessedArg = TypeVarArg | ParamSpec | tuple[TypeVarArg | ParamSpec, ...]
ProcessedArgs = Sequence[ProcessedArg]

# Type of mapping from type variables to their assignments.
TypeAssignments = dict[TypeVariable, ProcessedArg]

# Dummy ParamSpec to use to simplify substitution.
_P = ParamSpec('_P')

# Implicit values to use as standard collection bases, which do not supply type arguments to their bases.
IMPLICIT_BASES: dict[type[object], ParameterizedAlias | tuple[ParameterizedAlias, ...]] = {
    AbstractAsyncContextManager: Generic[T1],
    AbstractContextManager: Generic[T1],
    AsyncIterable: Generic[T1],
    AsyncIterator: AsyncIterable[T1],
    AsyncGenerator: (AsyncIterator[T1], Generic[T1, T2]),
    Awaitable: Generic[T1],
    Coroutine: (Awaitable[T3], Generic[T1, T2, T3]),
    Callable: Generic[P, T1],
    Container: Generic[T1],
    Iterable: Generic[T1],
    Iterator: Iterable[T1],
    Reversible: Iterable[T1],
    Generator: (Iterator[T1], Generic[T1, T2, T3]),
    Collection: (Container[T1], Iterable[T1]),
    ValuesView: Collection[T1],
    Sequence: (Collection[T1], Reversible[T1]),
    ByteString: Sequence[int],
    bytes: Sequence[int],
    memoryview: Sequence[int],
    MutableSequence: Sequence[T1],
    UserList: MutableSequence[T1],
    bytearray: MutableSequence[int],
    deque: MutableSequence[T1],
    list: MutableSequence[T1],
    range: Sequence[int],
    Set: Collection[T1],
    KeysView: Set[T1],
    ItemsView: Set[tuple[T1, T2]],
    frozenset: Set[T1],
    MutableSet: Set[T1],
    set: MutableSet[T1],
    Mapping: (Collection[T1], Generic[T1, T2]),
    MutableMapping: Mapping[T1, T2],
    ChainMap: MutableMapping[T1, T2],
    Counter: MutableMapping[T1, int],
    OrderedDict: MutableMapping[T1, T2],
    UserDict: MutableMapping[T1, T2],
    defaultdict: MutableMapping[T1, T2],
    dict: MutableMapping[T1, T2]
}

# Types whose assignments should be inherited from subclasses even if they don't appear in that class's mro (e.g., they
# are subclasses via a subclass hook).
HOOK_DEFAULTS: Final[tuple[TypeOrAlias, ...]] = ItemsView,


def all_vars(args: ProcessedArgs) -> TypeVariables:
    # Get all the unique parameters in order for each arg in args.
    seen = set()
    parameters = []
    for arg in args:
        for p in get_vars(arg):
            if p not in seen:
                seen.add(p)
                parameters.append(p)
    return tuple(parameters)


def apply(typ: GenericType, args: ProcessedArgs, vrs: TypeVariables) -> ParameterizedAlias:
    # Apply the given processed arguments to typ.
    # Prepare each argument so it is ready to be used in a type argument list.
    args = tuple(unprocess(arg) for arg in args)
    if typ is tuple:
        return tuple[args[0]]
    return typ[tuple(prepare(arg, var) for arg, var in zip(args, vrs))]


def clean(args: tuple[TypeVarArg | ProcessedArg, ...]) -> ProcessedArg:
    # After substitution, arguments are "cleaned" so that they are a flattened tuple where there are no consecutive
    # occurrences of _P, which is a placeholder for ellipsis objects (see "_process" below).
    new_arg = []
    last_was_ellipsis = False
    for arg in args:
        if arg is _P:
            if last_was_ellipsis:
                continue
            last_was_ellipsis = True
            new_arg.append(_P)
        else:
            last_was_ellipsis = False
            if isinstance(arg, tuple):
                new_arg += arg
            else:
                new_arg.append(arg)
    return tuple(new_arg)


def ensure_tuple(arg: T1) -> T1 | tuple[T1]:
    # Ensure that the given argument is a tuple.
    return arg if isinstance(arg, tuple) else (arg,)


def get_vars(arg: ProcessedArg) -> tuple[TypeVariable, ...]:
    # Get all the type parameters appearing in arg in order.
    # To accomplish this, arg, which is a tuple, is expanded inside a Concatenate between two instances of _P.
    # The first instance of _P ensures that the first parameter found is _P, allowing us to easily ignore it, even if
    # it appears in arg.
    # The second instance of _P ensures that the argument list ends in ParamSpec, without which the Concatenation will
    # fail to be created. [1:] discards the _P parameter.
    return Concatenate[(_P, *ensure_tuple(arg), _P)].__parameters__[1:]


def has_args(typ: TypeOrAlias) -> bool:
    # Determine if the given argument is a generic type alias with arguments.
    # "isinstance(base, type(List))" tests if base is a "_SpecialGenericAlias", the type of type aliases for
    # builtin collections. In this case, we want to treat base as though no type arguments were given.
    return typing.get_origin(typ) is not None and not isinstance(typ, type(List))


def is_generic(typ: type[object]) -> bool:
    # Determine if the given type is generic and can take type arguments.
    return (
        typ is type
        or typ is tuple
        or (typ in IMPLICIT_BASES and bool(Assignments.get(typ).vars))
        or (issubclass(typ, Generic) and bool(getattr(typ, '__parameters__', ())))
    )


def join(src: TypeAssignments, dest: TypeAssignments) -> TypeAssignments:
    # Join assignments in dest into assignments for src.
    return src | {var: sub_map(src, arg) for var, arg in dest.items()}


def orig_bases(typ: type[object]) -> tuple[TypeOrAlias, ...]:
    # Get typ.__orig_bases__ if it gave type arguments to its parameters. Otherwise, get typ.__bases__ (note that
    # typ.__orig_bases__ will be those for the class which previously gave type arguments to bases in the mro if typ
    # did not give any itself).
    if (implicit := IMPLICIT_BASES.get(typ)) is not None:
        return ensure_tuple(implicit)
    if (
        hasattr(typ, '__orig_bases__') and not
        any(getattr(base, '__orig_bases__', ()) is typ.__orig_bases__ for base in typ.__bases__)
    ):
        bases = typ.__orig_bases__
    else:
        bases = typ.__bases__
    mro = set(typ.mro(typ) if issubclass(typ, type) else typ.mro())
    bases_to_add = []
    for hook_default in HOOK_DEFAULTS:
        origin = typing.get_origin(hook_default) or hook_default
        if origin not in mro and issubclass(typ, origin):
            bases_to_add.append(hook_default)
    return bases + tuple(bases_to_add)


def prepare(arg: TypeArgs, var: TypeVariable) -> TypeArg:
    # Depending on the type of var, prepare arg for parameterization of a type.
    return arg[0] if isinstance(var, TypeVar) or arg == (...,) else list(arg)


def _process(arg: TypeArg) -> ProcessedArg:
    # "Process" the given type parameter argument so that it is a ProcessedArg, a flatten tuple of type arguments of
    # which none are ellipses. This processing is done because a ProcessedArg can be easily expanded in Concatenate for
    # convenient substitution and detection of type parameters. It also allows us to treat all type parameter arguments
    # as the same up to the point before we actually need to substitute them in a type argument list.
    if typing.get_origin(arg) is Concatenate:
        return _process(typing.get_args(arg))
    if isinstance(arg, (list, tuple)):
        # Substitute all ellipses for _P and clean the resulting tuple.
        return clean(tuple(_P if a is ... else a for a in arg))
    if arg is ...:
        return _P
    # Otherwise, just return the original argument.
    return arg


def process(arg: TypeArg, proxies: dict[TypeVariable, TypeVariable]) -> ProcessedArg:
    # Process the given arguments. Calls _process, and then substitutes any type parameters in the processed value with
    # their corresponding proxies.
    processed = _process(arg)
    vrs = get_vars(processed)
    return sub(tuple(proxies.get(var, var) for var in vrs), processed)


def sub(args: ProcessedArgs, target: ProcessedArg) -> ProcessedArg:
    # Substitute the given arguments for the type parameters in target.
    # Like params above, this uses a trick of expanding the arguments inside Concatenate with a _P on both sides.
    # The first _P ensures that _P is the first parameter so that we do not have to determine its position, while the
    # second _P ensures that the type arguments end in a ParamSpec, which is a requirement for Concatenate before
    # substitution. [1:-1] discards the extraneous _Ps.
    if not args:
        return target
    result = clean(typing.get_args(Concatenate[(_P, *ensure_tuple(target), _P)][(_P, *args)])[1:-1])
    if not isinstance(target, tuple):
        return result[0]
    return result


def sub_map(assignments: TypeAssignments, target: ProcessedArg) -> ProcessedArg:
    # Like sub, but uses a mapping from variables to arguments and infers uses that to infer the order in which to pass
    # the arguments.
    return sub(tuple(assignments.get(var, var) for var in get_vars(target)), target)


def type_arg_split(typ: TypeOrAlias) -> tuple[type[object], TypeArgs | None]:
    # Split the given type or alias into the origin type and the type arguments, or None if there aren't any.
    origin = typing.get_origin(typ)
    if origin is None or isinstance(typ, type(List)):
        return origin or typ, None
    return origin, typing.get_args(typ)


def unprocess(arg: ProcessedArg) -> TypeArgs:
    # Undo the processing logic (replace _P with ...) to prepare the argument for parameterization of a type.
    return typing.get_args(Concatenate[(*ensure_tuple(arg), _P)][...])[:-1]


def var_default(var: TypeVariable) -> ProcessedArg:
    # Get the default value for a type variable.
    return _P if isinstance(var, ParamSpec) else (Any, _P) if var is TP else Any


class Assignments:
    # Represents the type variable assignments for a particular type.
    # A cache is used for convenience and to ensure proxies are not created more than once for the same type.
    cache: Final[dict[TypeOrAlias, Assignments]] = {}

    # Mapping from assigned type variables to their assignments.
    args: TypeAssignments

    # Tuple of type variables that remain unassigned.
    vars: TypeVariables

    @staticmethod
    def from_bases(
        bases: tuple[TypeOrAlias, ...],
        proxies: OrderedDict[TypeVariable, TypeVariable] | None = None
    ) -> Assignments:
        # Core logic for constructing assignments for a type.
        proxies = proxies or OrderedDict()
        # Start with Assignments object with no assignments and all unassigned variables.
        assignments = Assignments({}, tuple(proxies.values()))
        for base in bases:
            # Iterate over all origin bases.
            origin, args = type_arg_split(base)
            if origin is Generic:
                # Processing Generic would be problematic: it's just parameterized with the type parameters of origin.
                continue
            # If base has arguments, process them. Otherwise default them.
            base_assignments = (
                Assignments.get(origin).default() if args is None else Assignments.get(origin).process(args, proxies)
            )
            # Update the assignments from the base class.
            assignments.update(base_assignments)
        return assignments

    @staticmethod
    def from_tuple_args(args: TypeArgs) -> Assignments:
        # Create an Assignments instance from tuple arguments.
        args = tuple(process(arg, OrderedDict()) for arg in args)
        new_args = {TP: args}
        if len(args) == 1 or (len(args) == 2 and args[1] is _P):
            new_args |= Assignments.get(Sequence[new_args[TP][0]]).args
        return Assignments(new_args, all_vars(args))

    @staticmethod
    def from_type_or_alias(typ: TypeOrAlias) -> Assignments:
        # Get an Assignments instance from a type or a type alias.
        origin, args = type_arg_split(typ)
        if origin is tuple:
            if args is None:
                return Assignments({}, (TP,))
            return Assignments.from_tuple_args(args)
        if args is None:
            bases = orig_bases(origin)
            for base in bases:
                if typing.get_origin(base) is Generic:
                    params = base.__parameters__
                    break
            else:
                params = all_vars(bases)
            proxies = OrderedDict((p, type(p)(p.__name__)) for p in params)
            return Assignments.from_bases(bases, proxies)
        return Assignments.get(origin).process(args, OrderedDict())

    @staticmethod
    def get(typ: TypeOrAlias) -> Assignments:
        # Get the Assignments object for the given type.
        if typ in Assignments.cache:
            return Assignments.cache[typ]
        return Assignments.cache.setdefault(typ, Assignments.from_type_or_alias(typ))

    def __init__(self, args: TypeAssignments | None = None, vrs: TypeVariables = ()) -> None:
        self.args = args or {}
        self.vars = vrs

    def apply(self, typ: GenericType) -> ParameterizedAlias:
        # Apply these assignments to the given type.
        if self.vars:
            # Default any unset variables before applying.
            return self.default().apply(typ)
        # Get assignments for typ.
        assignments = Assignments.get(typ)
        # It might seem like var.kind.default() is unnecessary assuming self corresponds to assignments for a subclass
        # of typ. However, it could be a subclass due to __subclasshook__, so use var.kind.default() in case this is
        # true.
        return apply(typ, tuple(self.args.get(var, var_default(var)) for var in assignments.vars), assignments.vars)

    def assign(self, args: ProcessedArgs) -> Assignments:
        # Return a new Assignments object which is the result of assigning each variable in self to args, in order.
        if self.is_tuple():
            if len(args) == 1 and isinstance(args[0], tuple):
                args = args[0]
            return Assignments.get(tuple[args])
        if len(args) != len(self.vars):
            raise TypeError('Wrong number of type arguments.')
        if not args:
            return self
        return Assignments(join(dict(zip(self.vars, args)), self.args), all_vars(args))

    def default(self) -> Assignments:
        # Return a new Assignments object with all unset variables defaulted.
        if not self.vars:
            return self
        return self.assign(tuple(var_default(var) for var in self.vars))

    def is_tuple(self) -> bool:
        # Determine whether this Assignments instance is for a tuple.
        return len(self.vars) == 1 and self.vars[0] is TP

    def process(self, args: TypeArgs, proxies: OrderedDict[TypeVariable, TypeVariable]) -> Assignments:
        # Process the given raw arguments.
        return self.assign(tuple(process(arg, proxies) for arg in args))

    def update(self, other: Assignments) -> None:
        # Update our assignments with the assignments in other and check their consistency.
        new_args = join(other.args, self.args)
        if any(new_args[var] != other.args[var] for var in other.args):
            raise TypeError('Inconsistent inheritance.')
        self.args = new_args


def infer_type_args(child: TypeOrAlias, parent: GenericType) -> ParameterizedAlias:
    # Return a parameterized alias whose type assignments are inferrable from child.
    origin = typing.get_origin(child) or child
    if not is_generic(parent):
        raise TypeError('parent is not generic.')
    if parent is type:
        return type[child]
    if not isinstance(origin, type):
        raise TypeError('child is not a type.')
    if not issubclass(origin, parent):
        raise TypeError('child is not a subclass of parent.')
    # Get all the variable assignments for child and apply them to parent.
    return Assignments.get(child).apply(parent)

在 Python3.10.0 上,infer_type_args 通过了以下测试:

import pytest
def test_infer_type_args() -> None:
    # These tests use some randomness in order to test a variety of types. Set a seed for consistent results.

    # Trivial test for built-in types first.
    assert infer_type_args(list, list) == list[Any]
    assert infer_type_args(list[int], list) == list[int]
    assert infer_type_args(dict, dict) == dict[Any, Any]
    assert infer_type_args(dict[int, str], dict) == dict[int, str]
    assert infer_type_args(dict[tuple[str, bool, float], dict[str, int]], dict) == (
        dict[tuple[str, bool, float], dict[str, int]]
    )

    # Thanks to get_origin, should also work with aliases defined in typing.
    assert infer_type_args(typing.List, list) == list[Any]
    assert infer_type_args(typing.List[str], list) == list[str]
    assert infer_type_args(typing.Dict, dict) == dict[Any, Any]
    assert infer_type_args(typing.Dict[int, str], dict) == dict[int, str]

    # Because issubclass(list, Iterable) is True due to subclass hooks, this works even though Iterable is not in
    # list.mro(). This is possible because infer_type_args recognizes that their type variables correspond to each
    # other.
    assert infer_type_args(list, Iterable) == Iterable[Any]
    assert infer_type_args(list[float], Iterable) == Iterable[float]
    assert infer_type_args(list[int], Sequence) == Sequence[int]
    assert infer_type_args(dict, Mapping) == Mapping[Any, Any]
    assert infer_type_args(dict[int, float], Mapping) == Mapping[int, float]
    assert infer_type_args(dict[str, bool], MutableMapping) == MutableMapping[str, bool]

    # Of course, collections defined in the standard modules collections and collections.abc do explicitly subclass each
    # other, so type parameters may be inferred for them too.
    assert infer_type_args(Iterable, Iterable) == Iterable[Any]
    assert infer_type_args(Iterable[int], Iterable) == Iterable[int]
    assert infer_type_args(Sequence[float], Iterable) == Iterable[float]
    assert infer_type_args(MutableMapping[str, Any], Mapping) == Mapping[str, Any]
    assert infer_type_args(OrderedDict[int, str], MutableMapping) == MutableMapping[int, str]

    # Try to infer tuple args, which are handled specially.
    # (Any, ...) are the default type arguments for tuple.
    assert infer_type_args(tuple, tuple) == tuple[Any, ...]
    assert infer_type_args(tuple[int, str, float], tuple) == tuple[int, str, float]
    assert infer_type_args(tuple, Iterable) == Iterable[Any]
    assert infer_type_args(tuple[int, str], Iterable) == Iterable[Any]
    # Tuples of uniform type correctly infer the element type argument for iterables.
    assert infer_type_args(tuple[int, ...], Iterable) == Iterable[int]
    assert infer_type_args(tuple[int], Iterable) == Iterable[int]

    # A few examples with subclasses of tuple.
    class TupleChildNoArgs(tuple): pass
    class TupleChildArgs(tuple[int, str, float]): pass
    class TupleChildArgsChild(TupleChildArgs): pass

    assert infer_type_args(TupleChildNoArgs, tuple) == tuple[Any, ...]
    assert infer_type_args(TupleChildArgs, tuple) == tuple[int, str, float]
    assert infer_type_args(TupleChildArgsChild, tuple) == tuple[int, str, float]

    # Do a few simple tests with Callable. More complicated tests are done below.
    assert infer_type_args(Callable, Callable) == Callable[..., Any]
    assert infer_type_args(Callable[[int, str], float], Callable) == Callable[[int, str], float]

    # Subclasses of Iterable or Iterator with additional type arguments.
    assert infer_type_args(Mapping, Iterable) == Iterable[Any]
    # First type argument of Mappings corresponds to Iterable, Collection, etc.
    assert infer_type_args(MutableMapping[int, float], Iterable) == Iterable[int]
    assert infer_type_args(Generator, Iterable) == Iterable[Any]
    # First type argument of Generator also corresponds to Iterable.
    assert infer_type_args(Generator[tuple[int, float], bool, NoneType], Iterable) == Iterable[tuple[int, float]]

    # Do the same with AsyncIterator.
    assert infer_type_args(AsyncGenerator, AsyncIterator) == AsyncIterator[Any]
    assert infer_type_args(AsyncGenerator[int, str], AsyncIterable) == AsyncIterable[int]

    # Coroutine is a subclass of Awaitable. The third type argument of Coroutine should be the same as the type
    # argument for Awaitable.
    assert infer_type_args(Coroutine, Awaitable) == Awaitable[Any]
    assert infer_type_args(Coroutine[int, str, float], Awaitable) == Awaitable[float]

    # Types.
    assert infer_type_args(int, type) == type[int]
    assert infer_type_args(str, type) == type[str]
    assert infer_type_args(AsyncGenerator[int, str], type) == type[AsyncGenerator[int, str]]

    # Test some special cases.
    assert infer_type_args(ByteString, Sequence) == Sequence[int]
    assert infer_type_args(Counter, Mapping) == Mapping[Any, int]
    assert infer_type_args(Counter[str], MutableMapping) == MutableMapping[str, int]
    assert infer_type_args(ItemsView, Iterable) == Iterable[tuple[Any, Any]]
    assert infer_type_args(ItemsView[int, str], Collection) == Collection[tuple[int, str]]
    assert infer_type_args(bytearray, MutableSequence) == MutableSequence[int]
    assert infer_type_args(bytes, Sequence) == Sequence[int]
    assert infer_type_args(memoryview, Sequence) == Sequence[int]
    assert infer_type_args(range, Sequence) == Sequence[int]

    # Thanks to subclass hooks, we can get desirable behavior by default even from non-generic subclasses.
    # Though, since they aren't actually generic, we can't actually check the results of parameterizations of them.
    assert infer_type_args(type(iter(())), Iterator) == Iterator[Any]
    assert infer_type_args(type(iter([])), Iterator) == Iterator[Any]
    assert infer_type_args(type(iter({})), Iterator) == Iterator[Any]
    assert infer_type_args(type(iter(set())), Iterator) == Iterator[Any]
    assert infer_type_args(type({}.keys()), KeysView) == KeysView[Any]
    assert infer_type_args(type({}.values()), ValuesView) == ValuesView[Any]
    assert infer_type_args(type({}.items()), ItemsView) == ItemsView[Any, Any]
    assert infer_type_args(type({}.items()), Iterable) == Iterable[tuple[Any, Any]]

    # No standard abstract base class specification is complete without this class.
    class VeryAbstractClass(
        AbstractAsyncContextManager, AbstractContextManager, AsyncGenerator, Coroutine, Generator, Mapping
    ): pass

    # Check that all relevant type parameters defaulted to Any.
    assert infer_type_args(VeryAbstractClass, AbstractAsyncContextManager) == AbstractAsyncContextManager[Any]
    assert infer_type_args(VeryAbstractClass, AbstractContextManager) == AbstractContextManager[Any]
    assert infer_type_args(VeryAbstractClass, AsyncGenerator) == AsyncGenerator[Any, Any]
    assert infer_type_args(VeryAbstractClass, Coroutine) == Coroutine[Any, Any, Any]
    assert infer_type_args(VeryAbstractClass, Generator) == Generator[Any, Any, Any]
    assert infer_type_args(VeryAbstractClass, Mapping) == Mapping[Any, Any]

    # Now with some type arguments specified.
    class LessAbstractClass(
        AbstractAsyncContextManager[NoneType],
        AbstractContextManager[bool],
        AsyncGenerator,
        Coroutine,
        Generator[tuple[bool, int], float, str],
        Mapping[tuple[bool, int], list[float]]
    ): pass

    # Check that type parameters with types omitted defaulted to Any, while explicitly specified ones were set
    # correctly.
    assert infer_type_args(LessAbstractClass, AbstractAsyncContextManager) == AbstractAsyncContextManager[NoneType]
    assert infer_type_args(LessAbstractClass, AbstractContextManager) == AbstractContextManager[bool]
    assert infer_type_args(LessAbstractClass, AsyncGenerator) == AsyncGenerator[Any, Any]
    assert infer_type_args(LessAbstractClass, Coroutine) == Coroutine[Any, Any, Any]
    assert infer_type_args(LessAbstractClass, Generator) == Generator[tuple[bool, int], float, str]
    assert infer_type_args(LessAbstractClass, Mapping) == Mapping[tuple[bool, int], list[float]]
    assert infer_type_args(LessAbstractClass, Iterable) == Iterable[tuple[bool, int]]

    # Now to actually try some examples with Generic.
    class Gen1(Generic[T1]): pass
    class Gen2(Gen1): pass
    class Gen3(Gen1[int]): pass
    class Gen4(Gen1[tuple[T1, T2]]): pass
    class Gen5(Gen4): pass
    class Gen6(Gen4[int, str]): pass
    class Gen7(Generic[T2]): pass
    class Gen8(Gen1, Gen7): pass
    class Gen9(Gen1[int], Gen7): pass
    class Gen10(Gen1, Gen7[int]): pass
    class Gen11(Gen1[int], Gen7[str]): pass

    assert infer_type_args(Gen1, Gen1) == Gen1[Any]
    assert infer_type_args(Gen1[int], Gen1) == Gen1[int]
    assert infer_type_args(Gen2, Gen1) == Gen1[Any]
    assert infer_type_args(Gen3, Gen1) == Gen1[int]
    assert infer_type_args(Gen4, Gen1) == Gen1[tuple[Any, Any]]
    assert infer_type_args(Gen4[int, str], Gen1) == Gen1[tuple[int, str]]
    assert infer_type_args(Gen5, Gen1) == Gen1[tuple[Any, Any]]
    assert infer_type_args(Gen5, Gen4) == Gen4[Any, Any]
    assert infer_type_args(Gen6, Gen1) == Gen1[tuple[int, str]]
    assert infer_type_args(Gen6, Gen4) == Gen4[int, str]
    assert infer_type_args(Gen8, Gen1) == Gen1[Any]
    assert infer_type_args(Gen8, Gen7) == Gen7[Any]
    assert infer_type_args(Gen9, Gen1) == Gen1[int]
    assert infer_type_args(Gen9, Gen7) == Gen7[Any]
    assert infer_type_args(Gen10, Gen1) == Gen1[Any]
    assert infer_type_args(Gen10, Gen7) == Gen7[int]
    assert infer_type_args(Gen11, Gen1) == Gen1[int]
    assert infer_type_args(Gen11, Gen7) == Gen7[str]

    # Try some more complicated examples with ParamSpec and Concatenate.
    # This does not work as expected if Generic appears before Callable as then this class inherits Callables behavior
    # when arguments are passed to it.
    # Note that inferred type param arguments will always be flattened and consequence occurrences of ... will collapse
    # into a single occurrence.
    P1 = ParamSpec('P1')
    P2 = ParamSpec('P2')
    P3 = ParamSpec('P3')
    class Complicated(Generic[P1, T1, T2, P2, P3], Callable[Concatenate[P1, T2, P2, P3], tuple[T1, T2]]): pass
    assert infer_type_args(Complicated, Complicated) == Complicated[..., Any, Any, ..., ...]
    assert infer_type_args(Complicated, Callable) == Callable[[..., Any, ...], tuple[Any, Any]]
    assert infer_type_args(Complicated[[int, str], bool, float, [NoneType, list[int]], [str, int]], Complicated) == (
        Complicated[[int, str], bool, float, [NoneType, list[int]], [str, int]]
    )
    assert infer_type_args(Complicated[[int, str], bool, float, [NoneType, list[int]], [str, int]], Callable) == (
        Callable[[int, str, float, NoneType, list[int], str, int], tuple[bool, float]]
    )
    assert infer_type_args(Complicated[[int, str], bool, float, ..., ...], Callable) == (
        Callable[[int, str, float, ...], tuple[bool, float]]
    )

像我一样正在寻找不需要修改受影响的 classes 的通用解决方案的读者,可能对以下更简单的解决方案感兴趣,在 Python 3.9:

from types import GenericAlias
from typing import Generic, TypeVar, Type, Union, get_origin, get_args

def resolve_type_arguments(query_type: Type, target_type: Union[Type, GenericAlias]) -> tuple[Union[Type, TypeVar], ...]:
    """
    Resolves the type arguments of the query type as supplied by the target type of any of its bases.
    Operates in a tail-recursive fashion, and drills through the hierarchy of generic base types breadth-first in left-to-right order to correctly identify the type arguments that need to be supplied to the next recursive call.

    raises a TypeError if they target type was not an instance of the query type.

    :param query_type: Must be supplied without args (e.g. Mapping not Mapping[KT,VT]
    :param target_type: Must be supplied with args (e.g. Mapping[KT, T] or Mapping[str, int] not Mapping)
    :return: A tuple of the arguments given via target_type for the type parameters of for the query_type, if it has any parameters, otherwise an empty tuple. These arguments may themselves be TypeVars.
    """
    target_origin = get_origin(target_type)
    if target_origin is None:
        if target_type is query_type:
            return ()
        else:
            target_origin = target_type
            supplied_args = None
    else:
        supplied_args = get_args(target_type)
        if target_origin is query_type:
            return supplied_args
    param_set = set()
    param_list = []
    for (i, each_base) in enumerate(target_origin.__orig_bases__):
        each_origin = get_origin(each_base)
        if each_origin is not None:
            for each_param in each_base.__parameters__:
                if each_param not in param_set:
                    param_set.add(each_param)
                    param_list.append(each_param)
            if issubclass(each_origin, query_type):
               if supplied_args is not None and len(supplied_args) > 0:
                    params_to_args = {key: value for (key, value) in zip(param_list, supplied_args)}
                    resolved_args = tuple(params_to_args[each] for each in each_base.__parameters__)
                    return resolve_type_arguments(query_type, each_base[resolved_args]) #each_base[args] fowards the args to each_base, it is not quite equivalent to GenericAlias(each_origin, resolved_args)
                else:
                    return resolve_type_arguments(query_type, each_base)
    if not issubclass(target_origin, query_type):
        raise ValueError(f'{target_type} is not a subclass of {query_type}')
    else:
        return ()

与@a_guest 的回答不同,此解决方案按需工作,不需要修改目标 classes。

例子

T = TypeVar('T')
U = TypeVar('U')
Q = TypeVar('Q')
R = TypeVar('R')

W = TypeVar('W')
X = TypeVar('X')
Y = TypeVar('Y')
Z = TypeVar('Z')

class A(Generic[T, U, Q, R]):
    ...
class NestedA(Generic[T, U, Q]):
    ...
class NestedB(Generic[T]):
    ...
class NoParams:
    ...
class B(NoParams, NestedA[U, Q, U], A[int, NestedA[Q, Q, Q], Q, U], NestedB[R]):
    ...
class C(B[T, str, int]):
    ...
class D(C[int]):
    ...
class E(D):
    ...
class F(E):
    ...

print(resolve_type_arguments(A, A))                          # (~T, ~U, ~Q, ~R)
print(resolve_type_arguments(A, A[W,X,Y,Z]))                 # (~W, ~X, ~Y, ~Z)
print(resolve_type_arguments(A, B))                          # (<class 'int'>, __main__.NestedA[~Q, ~Q, ~Q], ~Q, ~U)
print(resolve_type_arguments(A, B[W,X,Y]))                   # (<class 'int'>, __main__.NestedA[~X, ~X, ~X], ~X, ~W)
print(resolve_type_arguments(B, B))                          # (~U, ~Q, ~R)
print(resolve_type_arguments(B, B[W,X,Y]))                   # (~W, ~X, ~Y)
print(resolve_type_arguments(A, C))                          # (<class 'int'>, __main__.NestedA[str, str, str], <class 'str'>, ~T)
print(resolve_type_arguments(A, C[W]))                       # (<class 'int'>, __main__.NestedA[str, str, str], <class 'str'>, ~W)
print(resolve_type_arguments(B, C))                          # (~T, <class 'str'>, <class 'int'>)
print(resolve_type_arguments(B, C[W]))                       # (~W, <class 'str'>, <class 'int'>)
print(resolve_type_arguments(C, C))                          # (~T,)
print(resolve_type_arguments(C, C[W]))                       # (~W,)
print(resolve_type_arguments(A, D))                          # (<class 'int'>, __main__.NestedA[str, str, str], <class 'str'>, <class 'int'>)
print(resolve_type_arguments(B, D))                          # (<class 'int'>, <class 'str'>, <class 'int'>)
print(resolve_type_arguments(C, D))                          # (<class 'int'>,)
print(resolve_type_arguments(D, D))                          # ()
print(resolve_type_arguments(A, E))                          # (<class 'int'>, __main__.NestedA[str, str, str], <class 'str'>, <class 'int'>)
print(resolve_type_arguments(B, E))                          # (<class 'int'>, <class 'str'>, <class 'int'>)
print(resolve_type_arguments(C, E))                          # (<class 'int'>,)
print(resolve_type_arguments(D, E))                          # ()
print(resolve_type_arguments(E, E))                          # ()
print(resolve_type_arguments(A, F))                          # (<class 'int'>, __main__.NestedA[str, str, str], <class 'str'>, <class 'int'>)
print(resolve_type_arguments(B, F))                          # (<class 'int'>, <class 'str'>, <class 'int'>)
print(resolve_type_arguments(C, F))                          # (<class 'int'>,)
print(resolve_type_arguments(D, F))                          # ()
print(resolve_type_arguments(E, F))                          # ()
print(resolve_type_arguments(F, F))                          # ()

附加信息

这个新解决方案的关键在于 get_origin returns None for types other than generic aliases. Notably, expressions of the form type[var,...] are generic aliases and are therefore documented to have the __parameters__ attribute, as of Python 3.9。有趣的是,到目前为止,它们在技术上还不是 GenericAlias 的实例。我认为这允许 GenericAlias 被视为普通的 class.

请注意,根据 the documentation,即:

Note: If the getitem() of the class’ metaclass is present, it will take precedence over the class_getitem() defined in the class (see PEP 560 for more details).

请参阅 typing module and 以了解有关启用此解决方案的机制的更多信息以及一些与版本相关的重要警告。

2021 年 7 月 11 日更新

替换为更强大的解决方案并添加了更多有趣的示例。以前的版本在某些层次结构的一部分是非通用的情况下失败了。此外,新版本只能使用 Python 3.9 中完整记录的功能,其中的打字库现已稳定。