如何通过使用装饰器获取类泛型类型的运行时类型?

时间:2020-09-27 12:39:36

标签: python python-3.x generics annotations typing

我有一个通用类,例如

from typing import Generic, TypeVar, List

T = TypeVar('T')

@my_decorator
class Foo(Generic[T]):
    pass

f: Foo[int] = Foo()
g: Foo[List[float]] = Foo()

是否有一种干净的方法来获取装饰器中构造函数调用Foo[int], Foo[List[float]]的类型注释?我想在运行时进行类型检查。

我可以通过装饰器访问Foo的构造函数调用,甚至可以使用f: Foo[int] = Foo()inspect.stack()获得构造函数调用的代码行inspect.getsource(my_frame)以一种非常不漂亮的方式然后我可以通过一些字符串操作来获得Foo[int]

除了这是一种非常肮脏的方式之外,它仅以字符串形式获取类型。但是我需要实际的类型。在此示例中,我可以使用eval()“解析”字符串并将其转换为类型。但这不适用于自定义类,例如以下示例:

from typing import Generic, TypeVar, List

T = TypeVar('T')

class Bar:
    pass

@my_decorator
class Foo(Generic[T]):
    pass

h: Foo[List[Bar]] = Foo()

在这种情况下,我无法使用eval(),因为我不知道如何获得正确的context。 我想得到类似my_file.Foo[typing.List[my_file.Bar]]这样的东西,它可以让我在运行时进行类型检查。

那么有什么干净的方法吗?还是至少有一种(肮脏的)方式为eval()获取正确的上下文来“解析”字符串?

1 个答案:

答案 0 :(得分:1)

TL; DR:不可能在__init__中获得泛型的运行时类型,但是我们可以在CPython实现中获得足够的距离

  1. 要在类上仅使用装饰器来处理此问题,应将调用更改为h = Foo[List[Bar]](),以便装饰器可以独立于保存返回对象的变量来访问typehint。

  2. This answer表示通用类的类实例具有在{em> 初始化后可用的__orig_class__属性。此属性是在 初始化之后设置的(请参见source code)。

因此,如果我们编写类装饰器,则装饰器应修改源类以基本上在运行时设置__orig_class__属性时进行侦听。但是,这在很大程度上依赖于一个未记录的实现细节,并且在将来的Python版本或其他实现中可能无法以相同的方式工作。

def my_decorator(cls):
    orig_bases = cls.__orig_bases__
    genericType = orig_bases[0]
    class cls2(cls, genericType):
        def __init__(self, *args, **kwargs):
            super(cls2, self).__init__(*args, *kwargs)
        def __setattr__(self, name, value):
            object.__setattr__(self, name, value)
            if name == "__orig_class__":
                print("Runtime generic type is " + str(get_args(self.__orig_class__)))
    cls2.__orig_bases__ = orig_bases
    return cls2

然后:

>>> h = Foo[List[Bar]]()
Runtime generic type is (typing.List[__main__.Bar],)