Python 3.7:检查类型注释是否为通用类的“子类”

34

我正在尝试找到一种可靠且跨版本(3.5+)的方法,用于检查类型注释是否为给定泛型类型的“子类”(即从类型注释对象中获取泛型类型)。

在Python 3.5/3.6上,它可以正常运行,就像你期望的那样:

>>> from typing import List

>>> isinstance(List[str], type)
True

>>> issubclass(List[str], List)
True

在3.7版本上,泛型类型的实例似乎不再是type的实例,所以它将失败:

>>> from typing import List

>>> isinstance(List[str], type)
False

>>> issubclass(List[str], List)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.7/typing.py", line 716, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

脑海中涌现的其他想法是检查实际的实例类型,但是:

Python 3.6 / 3.5:

>>> type(List[str])
<class 'typing.GenericMeta'>

Python 3.7:

>>> type(List[str])
<class 'typing._GenericAlias'>

但这并没有进一步说明实际的通用类型是哪个(可能不是List);此外,以这种方式进行检查感觉相当错误,特别是因为_GenericAlias现在成为了“私有”类型(请注意下划线)。

另外一个可以检查的东西是类型的__origin__参数,但这也不像是正确的方法。

而且它在3.7上仍然有所不同:

>>> List[str].__origin__
<class 'list'>

当 3.5 / 3.6 为真时:

>>> List[str].__origin__
typing.List

我一直在寻找"正确"的方法来做这件事,但在 Python 文档/谷歌搜索中没有找到。

现在,我认为必须有一种清晰的方式来进行这个检查,因为像 mypy 这样的工具会依赖它来进行类型检查..?

更新:关于用例

好的,在这里添加一些更多的上下文..

所以,我使用函数签名内省(参数类型/默认值,返回类型,文档字符串)自动生成 GraphQL 模式(从而减少样板文件)。

我还有点犹豫这是否是一个好主意。

我喜欢它的可用性角度(不需要学习另一种声明函数签名的方式:只需按通常的方式注释类型);请参阅此处的两个代码示例,以了解我的意思:https://github.com/rshk/pyql

我想知道是否使用来自 typing 的通用类型(列表、字典、联合等)这种方式添加了太多的"黑魔法",可能会以意想不到的方式破坏它。(目前不是一个巨大的问题,但将来的 Python 版本是否过去 3.7?这将成为一个维护噩梦吗?)。

当然,另一种选择是仅使用自定义类型注释来支持更可靠/未来验证的检查,例如:https://github.com/rshk/pyql/blob/master/pyql/schema/types/core.py#L337-L339

..但缺点是这将强迫人们记住他们必须使用自定义类型注释。此外,我不确定 mypy 如何处理它(我假设需要在某个地方声明自定义类型与 typing.List 完全兼容..?还是听起来很 hackish)。

(我主要是请求关于这两种方法的建议,以及可能错过的任何优缺点。希望这不会对 SO 来说变得"太广泛"。)


1
你为什么要进行这个检查?你的使用场景是什么? - Martijn Pieters
1
我在这里看到的最大问题是没有定义处理typing内部的API。只有语法。静态类型检查器处理文本,而不是对象,因此它们不需要将List[str]作为对象处理。最多,该工具将从标记化输入构建AST。__origin__是未发布的实现细节(typing.py中的注释将其称为内部簿记),因此在自己的项目中依赖这些内容是自担风险。 - Martijn Pieters
似乎没有很好或官方的方法来做到这一点,但您可能会对 typing_inspect 库及其处理方式感兴趣。 - jonafato
@jonafato:我本来想提到 typing_inspect,但是在 Python 3.7 上该库也会给你返回 <class 'list'>,而在 Python 3.6 上则会返回 typing.List。并且它还不支持 Python 3.5。 - Martijn Pieters
1
@jonafato:typing_inspect 的优点在于它是由核心 mypy 贡献者开发的,一旦稳定下来,它可能会成为核心库的一部分。但我认为 OP 想要的目前无法实现,类型提示在 3.5 - 3.7 之间变化太大了。 - Martijn Pieters
请查看此答案以了解可调用类型 https://dev59.com/X0jSa4cB1Zd3GeqPCRfk#63134727 - adnanmuttaleb
3个回答

15
首先:没有定义API来内省由typing模块定义的类型提示对象。类型提示工具预期处理源代码,因此处理文本,而不是Python运行时的对象;mypy不会内省List[str]对象,而是处理您源代码的解析抽象语法树(Abstract Syntax Tree)

因此,虽然您总是可以访问像__origin__这样的属性,但实际上您正在处理实现细节(内部记账),而且那些实现细节可能会随版本而改变。

话虽如此,一个核心mypy / typing贡献者创建了typing_inspect模块来开发类型提示的内省API。该项目仍然被记录为实验性的,并且您可以预期它随着时间的推移而发生变化,直到不再是实验性的。它无法解决您在此处的问题,因为它不支持Python 3.5,并且其get_origin()函数返回与__origin__属性提供的完全相同的值。

在处理所有这些警告后,您想要访问的是Python 3.5 / Python 3.6中的__extra__属性;这是用于驱动最初实现(但自3.7以来已被删除)的issubclass() / isinstance()支持的基本内置类型:

def get_type_class(typ):
    try:
        # Python 3.5 / 3.6
        return typ.__extra__
    except AttributeError:
        # Python 3.7
        return typ.__origin__

在Python 3.5及以上版本中,此操作将产生<class 'list'>结果。不过,其仍然使用内部实现细节,并且很有可能在未来的Python版本中出现问题。


谢谢Martijin,我会关注“typing_inspect”,听起来一旦完成/稳定下来,那将是正确的方法。顺便说一下,我已经更新了我的问题,并提供了更多关于我的用例的上下文。 - redShadow
@redShadow:没错,你想用自己的类型系统映射来重载类型提示。我不确定这样做会持续多久。我知道@functools.singledispatch()类型提示支持故意避免支持泛型。 - Martijn Pieters
1
@redShadow:这里最大的问题是类型注释以及我们如何使用它们仍在不断变化,因为开发人员正在努力找出如何最好地表示实际项目中使用的类型,以及如何使所有这些在Python中正常工作。typing模块在Python 3.7中看到了很多更新,因为核心语言获得了一些钩子,专门用于避免需要typing工作的黑客攻击。因此,当您尝试以最初未设计的方式使用它时,您将看到更多的变化。 - Martijn Pieters
明白了。运行时内省的支持有可能会在未来某一天完全消失吗?这是我现在最担心的问题。看到“typing_inspect”和“@singledispatch”(我假设每个人都希望它也能支持泛型……?)发生这种情况的风险似乎接近于零,但是……? - redShadow
3
我真的说不好。你需要直接问开发人员。也许你可以尝试在Python-Dev邮件列表上发布问题。话虽如此,我非常怀疑运行时内省会消失,因为这与普遍的Python文化背道而驰。 - Martijn Pieters

14

Python 3.8增加了typing.get_origin()typing.get_args()以支持基本的内省。

这些API也已经被移植到Python >=3.5中的https://pypi.org/project/typing-compat/

请注意,当在裸泛型上调用typing.get_args时,其行为在3.7中仍然存在微妙的不同; 在3.8中,typing.get_args(typing.Dict)(),但在3.7中它是(~KT, ~VT)(其他通用类型类似),其中~KT~VT typing.TypeVar 类型的对象。


0
pip install typing_utils

那么

>>> typing_utils.issubtype(typing.List[int], list)
True

>>> typing_utils.issubtype(typing.List, typing.List[int])
False

typing_utils 还将 Python 3.8 中的 typing.get_origintyping.get_args 回溯到 3.6+。


2
typing_utils(https://pypi.org/project/typing_utils)刚刚2小时前发布,显然处于早期版本`0.0.1`,并且 setup.py中没有开发状态。您确定它足够稳定,已准备好用于生产吗?并且相对于typing v3.7.4.3包,主要优势是什么? - cizario

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