【问题标题】:What's the correct way to check if an object is a typing.Generic?检查对象是否是打字的正确方法是什么。通用?
【发布时间】:2018-08-16 16:37:52
【问题描述】:

我正在尝试编写验证类型提示的代码,为此我必须找出注释的对象类型。例如,考虑这个应该告诉用户期望什么样的值的 sn-p:

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 或其他资源支持的解决方案。通过访问未记录的内部属性来“工作”的“解决方案”很容易找到.但更有可能的是,它会变成一个实现细节,并且会在未来的版本中发生变化。我正在寻找“正确的方法”

【问题讨论】:

    标签: python generics type-hinting


    【解决方案1】:

    您可能正在寻找__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(2 年前)的令人放心的 quote

    我能推荐的最好的方法是使用__origin__——如果我们要更改这个属性,仍然需要一些其他的方法来访问相同的信息,并且很容易在你的代码中查找@的出现987654328@。 (与__extra__ 相比,我不太担心__origin__ 的更改。)您还可以查看_gorg()_geqv() 的内部函数(显然,这些名称不会成为任何公共API 的一部分,但是它们的实现非常简单并且在概念上很有用)。

    文档中的这一警告似乎表明大理石中尚未设置任何内容:

    如果核心开发人员认为有必要,即使在次要版本之间,也可能会添加新功能和 API。

    【讨论】:

    • 看起来很有希望。但看起来你也通过筛选源代码发现了这一点。如果可能的话,我更喜欢有一些文档或 PEP 或任何其他资源支持的解决方案,表明这不仅仅是一个实现细节。
    • 我没有任何文档或 PEP 引用它,因为 typing 是相当新的。有什么关于__args__ 的地方吗?我会注意的。
    • 遗憾的是,这在 Python 3.7 中出现了问题。 typing.Tuple[int, str].__origin__ 现在是 tuple 类,而不是 typing.Tuple 类。我还没有很好的选择:(你可以做一个糟糕的字符串比较,但是......见bugzilla.redhat.com/show_bug.cgi?id=1598574(这破坏了Fedora / RHEL安装程序!Whee。)
    【解决方案2】:

    没有获取此信息的官方方法。 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 版本 typing 模块反向端口的东西)。

    【讨论】:

    • Python v3.8 添加了函数typing.get_origintyping.get_args。这个选项似乎比使用它们的“魔法”属性对应物更可取。
    • @SonnyGarcia 太棒了,虽然 3 个版本太晚了!感谢您的提醒,一旦我有时间修改新功能,我会更新我的答案。
    • @Aran-Fey 正在更新您的答案 :)
    • 什么可以用于 Python 3.8,因为 _VariadicGenericAlias 不再存在?
    • Python >=3.5 有一个很好的兼容层,可以向后移植typing.get_origintyping.get_argspypi.org/project/typing-compat。请注意,typing.get_args 的行为在 3.7 中在裸泛型上调用时仍然存在细微差别;在 3.8 中 typing.get_args(typing.Dict)(),但在 3.7 中是 (~KT, ~VT)(对于其他泛型也类似),其中 ~KT~VTtyping.TypeVar 类型的对象。
    【解决方案3】:

    正如 cmets 中的 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
    

    您可以在docs找到更多详细信息

    【讨论】:

      【解决方案4】:

      我认为,您最多可以做的就是在变量上使用typ,在变量上使用typing.get_type_hints,然后从返回的类似__annotations__ 的字典中提取您需要的信息。

      PEP-484 说:

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

      26.1.7. Classes, functions, and decorators 说:

      在运行时,isinstance(x, T) 将引发 TypeError。通常,isinstance()issubclass() 不应与类型一起使用。

      但是,PEP-526 在“非目标”中说:

      虽然该提案附带了 typing.get_type_hints 标准库函数的扩展,用于运行时检索注解,但变量注解不是为运行时类型检查而设计的。必须开发第三方软件包来实现此类功能。

      【讨论】:

      • 我可能会误解,但我不明白get_type_hints 会如何帮助我?如果我定义一个变量x: typ,然后在其上使用get_type_hints,我将得到typing.Union[int, str]作为结果。
      • 嗯,在运行时,你有你正在寻找的'int'和'str'。
      • 或进一步检查结果以制作更详细的输出,就像您尝试使用 isinstanceissubclass 的代码一样。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-03
      • 2011-03-27
      • 1970-01-01
      • 2019-09-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多