Generic[T] base class - 如何从实例中获取 T 的类型?

Generic[T] base class - how to get type of T from within instance?

假设您有一个 Python class 继承自 Generic[T]。有什么方法可以获取从 class/instance?

中传入的实际类型

例如,

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"

,建议类型 arg 出现在类型的 args 字段中。事实上,

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

将打印 (,)。但是,我似乎无法直接从实例中访问它,例如替换

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

似乎没有成功。

谢谢

API 不支持此功能。在有限的情况下,如果你愿意乱搞未记录的实现细节,你有时可以这样做,但它根本不可靠。


首先,mypy 不要求您在分配给通用类型变量时提供类型参数。你可以做像 x: Test[int] = Test() 这样的事情,Python 和 mypy 都不会抱怨。 mypy 推断类型参数,但在运行时使用 Test 而不是 Test[int]。由于显式类型参数难以编写并且会带来性能损失,因此许多代码仅在注释中使用类型参数,而不是在运行时使用。

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


当在运行时提供类型参数时,实现 确实 当前尝试保留此信息,但仅在完全未记录的内部属性中,如有更改,恕不另行通知,并且甚至这个属性也可能不存在。具体来说,当你打电话给

Test[int]()

,新对象的 class 是 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 版本而异,但目前,它是 set 在正常对象构造已经完成之后。在 __init____new__ 期间,您将无法检查 __orig_class__

这些实现细节是截至 CPython 3.8.2.

的最新版本

__orig_class__ 是一个实现细节,但至少在 Python 3.8 上,您不必访问任何其他实现细节来获取类型参数。 Python 3.8 引入了 typing.get_args,其中 returns 是 typing 类型参数的元组,或 () 无效参数。 (是的,从 Python 3.5 到 3.8 一直没有 public API。)

例如,

typing.get_args(Test[int]().__orig_class__) == (int,)

如果 __orig_class__ 存在并且您愿意访问它,那么 __orig_class__get_args 一起提供您正在寻找的内容。

您可以使用 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

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

# 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)

如果需要,请使用它。您可以在整个 class 中使用类型变量,即使不使用 self.只是说,语法高亮器可能很难理解这类事情,因为它使用了他们并不真正关心的东西的 return,即使它是 class。至少对于pylance,因为那是我使用的。

如果您愿意稍微调整 class 实例化的语法,这是可能的。

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 的运行时值开始,让类型检查器推断实例的类型是 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"])

目前 (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'>"