泛型[T]基类 - 如何在实例内部获取T的类型?

45
假设你有一个继承自 Generic[T] 的 Python 类。有没有办法从类/实例中获取传递的实际类型?
例如,
from typing import TypeVar, Type
T = TypeVar('T')

class Test(Generic[T]):
    def hello(self):
      my_type = T  # this is wrong!
      print( "I am {0}".format(my_type) )

Test[int]().hello() # should print "I am int"

这里,建议类型参数将出现在类型的args字段中。确实如此,

print( str( Test[int].__args__ ) )

会打印出 (<class 'int'>,)。然而,我似乎无法直接从实例中访问它,例如替换

      my_type = self.__class__.__args__ # this is also wrong (None)

似乎没有起到作用。

谢谢。


你需要使用 type 方法吗?比如 my_type = type(T) - DavidG
2
很遗憾,type(T)将是typing.TypeVar。 - Bogey
6个回答

42

目前没有支持该操作的API。在非常有限的情况下,如果您愿意尝试未经记录的实现细节,有时可能可以做到,但这完全不可靠。


首先,mypy在将泛型类型分配给变量时不要求您提供类型参数。您可以像 x: Test[int] = Test() 这样做,既不会引起Python也不会引起mypy的投诉。mypy会推断类型参数,但是在运行时使用的是 Test 而不是 Test[int]。由于显式类型参数很麻烦而且会带来性能损失,很多代码只在注释中使用类型参数,而不在运行时使用。

无法从未在运行时提供的类型参数中恢复信息。


当在运行时提供类型参数时,实现目前确实尝试保留此信息,但仅在完全未记录的内部属性中进行,并且该属性甚至可能不存在。具体来说,当您调用

Test[int]()

新对象的类是Test而不是Test[int],但typing实现尝试设置

obj.__orig_class__ = Test[int]

在新对象上,如果无法设置__orig_class__(例如,如果Test使用了__slots__),那么它会捕获AttributeError并放弃。

__orig_class__是在Python 3.5.3中引入的;在3.5.2及以下版本中不存在。在typing中没有任何实际使用__orig_class__的内容。

__orig_class__的分配时间因Python版本而异,但目前,它是在普通对象构造已经完成后设置的。您将无法在__init____new__期间检查__orig_class__

这些实现细节截至CPython 3.8.2为止。


__orig_class__是一个实现细节,但至少在Python 3.8上,您不必访问任何其他实现细节即可获取类型参数。Python 3.8引入了typing.get_args,它返回typing类型的类型参数元组,对于无效参数则返回()。(是的,从Python 3.5到3.8一直没有公共API可用于此。)

例如,

typing.get_args(Test[int]().__orig_class__) == (int,)
如果存在__orig_class__并且您愿意访问它,则__orig_class__get_args一起提供您所需的内容。

这确实回答了问题,但我仍有疑问:在对象的生命周期的哪个阶段__orig_clas__变为可用?我想在__init__期间访问该信息,但从我到目前为止尝试的情况来看,即使在调用super().__init__()之后,也似乎不可用。显然,这就是另一个问题。 - Carter Pape
1
@CarterPape:取决于你的Python版本,但目前它设置在typing._GenericAlias.__call__中,在普通对象构造完成并且所有__new____init__方法执行完毕后。 - user2357112
是的,我刚刚浏览了一下找到它的设置位置,感谢你指出。 __call__ 是通过什么黑魔法被调用的?此外,我已经创建了一个单独的问题来继续讨论。 - Carter Pape

12

目前(Python 3.10.3),在__init____new__期间无法访问类型参数。

然而,在__init_subclass__中可以访问类型变量。这是一个有点不同的场景,但我认为它足够有趣可以分享一下。

from typing import Any, Generic, TypeVar, get_args

T = TypeVar("T")


class MyGenericClass(Generic[T]):
    _type_T: Any

    def __init_subclass__(cls) -> None:
        cls._type_T = get_args(cls.__orig_bases__[0])[0]  # type: ignore


class SomeBaseClass(MyGenericClass[int]):
    def __init__(self) -> None:
        print(self._type_T)


SomeBaseClass()  # prints "<class 'int'>"

8

您可以使用self.__orig_class__

from typing import TypeVar, Type, Generic
T = TypeVar('T')

class Test(Generic[T]):

    def hello(self):
        print( "I am {0}".format(self.__orig_class__.__args__[0].__name__))

Test[int]().hello()
# I am int

7
如果你愿意稍微调整类实例化的语法,这是可以实现的。
import typing

T = typing.TypeVar('T')
class MyGenericClass(typing.Generic[T]):
    def __init__(self, generic_arg: typing.Type[T]) -> None:
        self._generic_arg = generic_arg

    def print_generic_arg(self) -> None:
        print(f"My generic argument is {self._generic_arg}")

    def print_value(self, value: T) -> None:
        print(value)

my_instance = MyGenericClass(int)  # Note, NOT MyGenericClass[int]().

my_instance.print_generic_arg()  # Prints "My generic argument is <class 'int'>".

reveal_type(my_instance)  # Revealed type is MyGenericClass[builtins.int*].
                          # Note the asterisk, indicating that 
                          # builtins.int was *inferred.*

my_instance.print_value(123)  # OK, prints 123.
my_instance.print_value("abc")  # Type-checking error, "abc" isn't an int.

作为这里的另一个答案所解释的一样,从实例类型中尝试在运行时检索类型参数可能本质上存在问题。部分原因是,该实例并不一定是最初创建时带有类型参数的。
因此,这种方法通过另一种方式来解决这个问题。我们要求调用者将类型作为参数传递给__init__然后,我们不再尝试从实例类型MyGenericClass[int]中推导出int的运行时值,而是从int的运行时值开始,并让类型检查器推断实例的类型为MyGenericClass[int]
在更复杂的情况下,您可能需要通过冗余指定类型来帮助类型检查器。您需要确保类型匹配。
possible_types = {"int": int, "str": str}

# Error: "Need type annotation".
my_instance = MyGenericClass(possible_types["int"])

# OK.
my_instance = MyGenericClass[int](possible_types["int"])

class PatternArg(Generic[GT_PatternArg]): def __new__( cls, type_: Type[GT_PatternArg], default: Optional[GT_PatternArg] = None ): self.type_ = type_ self.default = default return cast(GT_PatternArg, "PatternArg")它可以结合 __new__,从而返回任意类型,而 __init__ 只能返回实例本身。 - S_SmJaE

2

PEP560 引入了 cls.__orig_bases__,且 typing 模块提供了 get_originget_args 两个实用函数来提取类型。然而,当你有一个复杂的类层次结构并具有多级泛型继承时,你需要遍历该层次结构并将具体类型参数向下传递到层次结构中。

例如,如果你有以下类:

from typing import Generic, Literal, LiteralString
from datetime import date

T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)

class Thing(Generic[T, L]):
    pass

class ChildThing(Thing[T, L], Generic[T, L]):
    pass

class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
    pass

class DateThing(StringThing[Literal["date"], date]):
    pass

然后:

print(get_args(DateThing.__orig_bases__[0]))

将输出:

(typing.Literal['date'], <class 'datetime.date'>)

并且不会为基类Thing的参数T提供类型。为此,您需要更复杂的逻辑:

from typing import (
    Any,
    Dict,
    Generic,
    Literal,
    LiteralString,
    Optional,
    Tuple,
    TypeVar,
    get_args,
    get_origin,
)
from datetime import date

T = TypeVar("T")
L = TypeVar("L", bound=LiteralString)
F = TypeVar("F", bound=str)

def get_generic_map(
    base_cls: type,
    instance_cls: type,
) -> Dict[Any, Any]:
    """Get a map from the generic type paramters to the non-generic implemented types.

    Args:
        base_cls: The generic base class.
        instance_cls: The non-generic class that inherits from ``base_cls``.

    Returns:
        A dictionary mapping the generic type parameters of the base class to the
        types of the non-generic sub-class.
    """
    assert base_cls != instance_cls
    assert issubclass(instance_cls, base_cls)
    cls: Optional[type] = instance_cls
    generic_params: Tuple[Any, ...]
    generic_values: Tuple[Any, ...] = tuple()
    generic_map: Dict[Any, Any] = {}

    # Iterate over the class hierarchy from the instance sub-class back to the base
    # class and push the non-generic type paramters up through that hierarchy.
    while cls is not None and issubclass(cls, base_cls):
        if hasattr(cls, "__orig_bases__"):
            # Generic class
            bases = cls.__orig_bases__

            # Get the generic type parameters.
            generic_params = next(
                (
                    get_args(generic)
                    for generic in bases
                    if get_origin(generic) is Generic
                ),
                tuple(),
            )

            # Generate a map from the type parameters to the non-generic types pushed
            # from the previous sub-class in the hierarchy.
            generic_map = (
                {param: value for param, value in zip(generic_params, generic_values)}
                if len(generic_params) > 0
                else {}
            )

            # Use the type map to push the concrete parameter types up to the next level
            # of the class hierarchy.
            generic_values = next(
                (
                    tuple(
                        generic_map[arg] if arg in generic_map else arg
                        for arg in get_args(base)
                    )
                    for base in bases
                    if (
                        isinstance(get_origin(base), type)
                        and issubclass(get_origin(base), base_cls)
                    )
                ),
                tuple(),
            )
        else:
            generic_map = {}

        assert isinstance(cls, type)
        cls = next(
            (base for base in cls.__bases__ if issubclass(base, base_cls)),
            None,
        )

    return generic_map


class Thing(Generic[T, L]):
    type: T
    literal: L
    def __init__(self) -> None:
        super().__init__()
        type_map = get_generic_map(Thing, type(self))
        self.type = type_map[T]
        self.literal = type_map[L]

    def hello(self) -> str:
        return f"I am type={self.type}, literal={self.literal}"

class ChildThing(Thing[T, L], Generic[T, L]):
    pass

class StringThing(ChildThing[T, Literal["string"]], Generic[F, T]):
    def __init__(self) -> None:
        super().__init__()
        type_map = get_generic_map(StringThing, type(self))
        self.f = type_map[F]

    def hello(self) -> str:
        return f"{super().hello()}, f={self.f}"

class DateThing(StringThing[Literal["date"], date]):
    pass

thing = DateThing()
print(thing.hello())

输出结果为:

I am type=<class 'datetime.date'>, literal=typing.Literal['string'], f=typing.Literal['date']

-1

你可以完全使用其他东西。

# Define Wrapper Class
class Class():
    # Define __getitem__ method to be able to use index
    def __getitem__(self, type):
        # Define Main Class
        class Class():
            __doc__ = f"""I am an {type.__name__} class"""

            def __init__(self, value):
                self.value: type = type(value)
        # Return Class
        return Class
# Set Class to an instance of itself to be able to use the indexing
Class = Class()

print(Class[int].__doc__)
print(Class[int](5.3).value)

如果需要的话可以使用这个。你可以在整个类中使用type变量,即使不使用self。只是说,语法高亮可能会很难理解这种情况,因为它使用了一个返回值,而他们并不真正关心,即使它是一个类。至少对于我使用的pylance来说是这样。


这正是我想要的解决方案 - Jotarata
我不理解在__getitem__内重新定义Class - 这不像是一个好的做法。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接