如何正确检查对象是否为typing.Generic?

61

我试图编写一个可以验证类型提示的代码,为此我需要找出注释所表示对象的类型。例如,考虑下面的代码片段,它应该告诉用户期望的值是什么类型:

import typing

typ = typing.Union[int, str]

if issubclass(typ, typing.Union):
    print('value type should be one of', typ.__args__)
elif issubclass(typ, typing.Generic):
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

这应该打印出“值类型应为(int,str)之一”,但实际上它抛出了一个异常:

Traceback (most recent call last):
  File "untitled.py", line 6, in <module>
    if issubclass(typ, typing.Union):
  File "C:\Python34\lib\site-packages\typing.py", line 829, in __subclasscheck__
    raise TypeError("Unions cannot be used with issubclass().")
TypeError: Unions cannot be used with issubclass().

isinstance 也不起作用:

>>> isinstance(typ, typing.Union)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python34\lib\site-packages\typing.py", line 826, in __instancecheck__
    raise TypeError("Unions cannot be used with isinstance().")
TypeError: Unions cannot be used with isinstance().

如何正确地检查typ是否为typing.Generic

如果可能,我希望看到一个有文档、有PEP或其他资源支持的解决方案。通过访问未记录的内部属性来“解决”问题很容易。但更可能的是,它将被证明是一项实现细节,并且将在未来的版本中更改。我正在寻找“正确的方法”

6个回答

47

你可能正在寻找__origin__

# * __origin__ keeps a reference to a type that was subscripted,
#   e.g., Union[T, int].__origin__ == Union;`

import typing

typ = typing.Union[int, str]

if typ.__origin__ is typing.Union:
    print('value type should be one of', typ.__args__)
elif typ.__origin__ is typing.Generic:
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

>>>value type should be one of (<class 'int'>, <class 'str'>)

我能找到的最好的支持使用这个未记录属性的方法是来自 Guido Van Rossum(2016 年)的quote
“我最好的建议是使用 __origin__——如果我们改变了这个属性,仍然必须有其他方法来访问相同的信息,并且很容易在代码中搜索出 __origin__ 的出现。 (我对 __origin__ 的更改比对 __extra__ 的更改不太担心。)您还可以查看内部函数 _gorg()_geqv()(显然,这些名称将不会成为任何公共 API 的一部分,但它们的实现非常简单且具有概念上的用处)。”
文档中的这个警告似乎表明仍没有确定的事情:
“如果核心开发人员认为必要,甚至在次要版本之间也可能添加新功能并更改 API。”

看起来很有前途。但是你似乎是通过查看源代码找到的。如果可能的话,我更喜欢一个有文档支持、PEP或其他资源支持的解决方案,以表明这不仅仅是实现细节。 - Aran-Fey
我没有任何文档或 PEP 参考 typing,因为它是相当新的。是否有任何关于 __args__ 的信息?我会继续寻找。 - Jacques Gaudin
13
可悲的是,这在Python 3.7中已经失效了。typing.Tuple[int, str].__origin__现在是类tuple,而不是类typing.Tuple。目前我还没有更好的替代方法 :( 尽管可以进行可行的字符串比较,但...请参见https://bugzilla.redhat.com/show_bug.cgi?id=1598574 (这损坏了Fedora / RHEL安装程序!) - Adam Williamson

26

正如在评论中由sonny-garcia指出的那样,get_origin()适用于python 3.8及以上版本。

import typing
from typing import get_origin

typ = typing.Union[int, str]
get_origin(typ) == typing.Union
#True

您可以在文档中找到更多细节


21

目前没有任何官方途径获取这些信息。 typing 模块仍在积极开发中,没有公共 API。(事实上,它可能永远不会有。)我们能做的就是查看模块内部,并找到获取所需信息的最简洁方式。由于该模块仍在开发中,其内部将会有很多变化。


在 Python 3.5 和 3.6 中,泛型有一个 __origin__ 属性,其中保存了对原始泛型基类的引用(即,List[int].__origin__ 将是 List),但这在3.7中已更改。现在,最简单的检查某个东西是否为泛型的方法可能是检查其 __parameters____args__ 属性。

以下是一组可用于检测泛型的函数:

import typing


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _is_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            return True

        if isinstance(cls, typing._SpecialForm):
            return cls not in {typing.Any}

        return False


    def _is_base_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            if cls.__origin__ in {typing.Generic, typing._Protocol}:
                return False

            if isinstance(cls, typing._VariadicGenericAlias):
                return True

            return len(cls.__parameters__) > 0

        if isinstance(cls, typing._SpecialForm):
            return cls._name in {'ClassVar', 'Union', 'Optional'}

        return False
else:
    # python <3.7
    if hasattr(typing, '_Union'):
        # python 3.6
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union, typing._Optional, typing._ClassVar)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union)):
                return cls.__args__ in {None, ()}

            if isinstance(cls, typing._Optional):
                return True

            return False
    else:
        # python 3.5
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing.UnionMeta, typing.OptionalMeta, typing.CallableMeta, typing.TupleMeta)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, typing.GenericMeta):
                return all(isinstance(arg, typing.TypeVar) for arg in cls.__parameters__)

            if isinstance(cls, typing.UnionMeta):
                return cls.__union_params__ is None

            if isinstance(cls, typing.TupleMeta):
                return cls.__tuple_params__ is None

            if isinstance(cls, typing.CallableMeta):
                return cls.__args__ is None

            if isinstance(cls, typing.OptionalMeta):
                return True

            return False


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This includes "special" types like
    Union and Tuple - anything that's subscriptable, basically.
    """
    return _is_generic(cls)


def is_base_generic(cls):
    """
    Detects generic base classes, for example `List` (but not `List[int]`)
    """
    return _is_base_generic(cls)


def is_qualified_generic(cls):
    """
    Detects generics with arguments, for example `List[int]` (but not `List`)
    """
    return is_generic(cls) and not is_base_generic(cls)

所有这些函数都应该在所有Python版本<=3.7中正常工作(包括任何使用typing模块后移的版本<3.5)。


12
Python v3.8 新增了 typing.get_origintyping.get_args 函数。使用这些函数似乎比使用其“神奇”属性对应项更好。 - Sonny Garcia
@SonnyGarcia 非常棒,虽然晚了3个版本!感谢您的提醒,我会在有时间尝试新功能后更新我的答案。 - Aran-Fey
1
@Aran-Fey,请更新您的回答 :) - alkasm
1
Python 3.8中不再存在 _VariadicGenericAlias,那么现在可以用什么来代替它呢? - Hernan
有一个非常棒的 Python >=3.5 兼容层,可以回溯typing.get_origintyping.get_args: https://pypi.org/project/typing-compat/。请注意,在裸通用类型上调用`typing.get_args`的行为在 3.7 中仍然有微妙的区别; 在 3.8 中,typing.get_args(typing.Dict)(), 但在 3.7 中它是(~KT, ~VT)(其他通用类型同理),其中~KT~VTtyping.TypeVar类型的对象。 - Max Gasner

2
截至Python 3.8版本,typing.get_origin(tp)是正确的方法。该函数的文档字符串非常详细(如果不属于typing则返回None)。
def get_origin(tp):
    """Get the unsubscripted version of a type.

    This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
    and Annotated. Return None for unsupported types. Examples::

        get_origin(Literal[42]) is Literal
        get_origin(int) is None
        get_origin(ClassVar[int]) is ClassVar
        get_origin(Generic) is Generic
        get_origin(Generic[T]) is Generic
        get_origin(Union[T, int]) is Union
        get_origin(List[Tuple[T, T]][int]) == list
    """

在您的应用中,它可能会是这样的:

import typing

def func(typ):
    if typing.get_origin(typ) is typing.Union:
        print('value type should be one of', typing.get_args(typ))
    elif typing.get_origin(typ) is typing.Generic:
        print('value type should be a structure of', typing.get_args(typ))
    else:
        print('value type should be', typ)

A = typing.TypeVar("A")
B = typing.TypeVar("B")

func(typing.Union[int, str])
func(typing.Generic[A, B])
func(int)

>>> "value type should be one of (<class 'int'>, <class 'str'>)"
>>> "value type should be a structure of (~A, ~B)"
>>> "value type should be <class 'int'>"

0

我在我的一个项目中编写了这个程序:

def is_type(arg: object) -> bool:
    """Return True if `arg` is a type, including those for generic typing."""
    from typing import Any, get_origin

    generic_base = type(Any)
    if (aux := generic_base.__base__) is not object:
        generic_base = aux
    return isinstance(arg, (type, generic_base)) or get_origin(arg)

-1

我认为你最多能做的就是在变量上使用typ,在其上使用typing.get_type_hints,并从返回的类似于__annotations__的字典中提取所需信息。

PEP-484说:

get_type_hints()是一个实用函数,用于从函数或方法中检索类型提示。给定一个函数或方法对象,它返回一个与__annotations__格式相同的字典,但将前向引用(以字符串文字形式给出)作为表达式在原始函数或方法定义的上下文中进行评估。

26.1.7. 类、函数和装饰器说:

在运行时,isinstance(x, T)会引发TypeError。一般来说,不应该使用isinstance()issubclass()与类型一起使用。

然而,PEP-526 在“非目标”中指出:

虽然该提案伴随着 typing.get_type_hints 标准库函数的扩展,以便在运行时检索注释,但变量注释并不是为了进行运行时类型检查而设计的。第三方包将需要开发来实现这样的功能。


1
我可能误解了,但我不明白 get_type_hints 怎么能帮助我?如果我定义一个变量 x: typ,然后在它上面使用 get_type_hints,我只会得到 typing.Union[int, str] 作为结果。 - Aran-Fey
在运行时,您已经拥有了您正在寻找的'int'和'str'。 - edixon
或者进一步检查结果,以制作更详细的输出,就像您的代码尝试使用isinstanceissubclass一样。 - edixon

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