【问题标题】:How to access the type arguments of typing.Generic?如何访问 typing.Generic 的类型参数?
【发布时间】:2018-07-12 08:57:26
【问题描述】:

typing 模块为泛型类型提示提供了一个基类:typing.Generic 类。

Generic 的子类接受方括号中的类型参数,例如:

list_of_ints = typing.List[int]
str_to_bool_dict = typing.Dict[str, bool]

我的问题是,如何访问这些类型参数?

也就是说,给定str_to_bool_dict 作为输入,我怎样才能得到strbool 作为输出?

基本上我正在寻找这样的功能

>>> magic_function(str_to_bool_dict)
(<class 'str'>, <class 'bool'>)

【问题讨论】:

  • 请注意,相同的方法(.__args__ (almost undocumented)、typing.get_args())也适用于 typing 模块中的其他内容(_GenericAlias_SpecialForm - 其中包含 @ 987654338@、NoReturnClassVarUnionOptional)。 get_args(Optional[str]) 也返回与 get_args(Union[str, NoneType]) 相同的内容。

标签: python generics


【解决方案1】:

Python >= 3.8

从 Python3.8 开始有 typing.get_args:

print( get_args( List[int] ) ) # (<class 'int'>,)

PEP-560 还提供了__orig_bases__[n],它允许我们使用第 n 个泛型基的参数:

from typing import TypeVar, Generic, get_args

T = TypeVar( "T" )

class Base( Generic[T] ):
    pass

class Derived( Base[int] ):
    pass

print( get_args( Derived.__orig_bases__[0] ) ) # (<class 'int'>,)

Python >= 3.6

从 Python 3.6 开始。有一个公共 __args__ 和 (__parameters__) 字段。 例如:

print( typing.List[int].__args__ )

这包含泛型参数(即int),而__parameters__ 包含泛型本身(即~T)。


Python

使用typing_inspect.getargs


一些注意事项

typing 跟随PEP8。 PEP8 和typing 均由 Guido van Rossum 合着。双前导和尾随下划线定义为:“存在于用户控制的命名空间”中的“魔术”对象或属性

dunders 也被在线评论;来自typing的官方存储库我们 可以看到:

  • __args__ 是下标中使用的所有参数的元组,例如,Dict[T, int].__args__ == (T, int)”。

但是,authors also note:

  • “打字模块具有临时状态,因此它不受向后兼容性的高标准覆盖(尽管我们尽量保持它),对于(尚未记录的)dunder 属性尤其如此比如__union_params__。如果您想在运行时上下文中使用类型,那么您可能会对typing_inspect 项目感兴趣(其中一部分可能会在稍后键入)。”

一般来说,无论您对typing 做什么,都需要暂时保持最新状态。如果您需要前向兼容的更改,我建议您编写自己的注释类。

【讨论】:

  • 你怎么知道这是公开的?如果您有任何来源,请引用它们。
  • “你怎么知道这是公开的” - 因为__xxx__ 始终是公开的,而且与_xxx__xxx 不同,从未提出其他建议。我想你的意思是“你怎么知道这是记录在案的”。我已经用文档更新了我的答案。
  • 如果类仅在类型存根 (.pyi) 文件中是泛型而不是实际的运行时类,是否有办法访问泛型类型?
  • 从基类中,我能够使用T: Type[Any]= get_args(self.__orig_bases__[0])[0] 检索类型,其中T 在实际类型中。谢谢!
【解决方案2】:

据我所知,这里没有满意的答案。

想到的是存储此信息的__args__ undocumented 属性:

list_of_ints.__args__
>>>(<class 'int'>,)

str_to_bool_dict.__args__
>>>(<class 'str'>, <class 'bool'>)

typing 模块的文档中没有提及它。

值得注意的是,它在文档中是very close to be mentioned

也许我们还应该讨论是否需要记录GenericMeta.__new__ 的所有关键字参数。有tvarsargsoriginextraorig_bases。我想我们可以谈谈前三个(它们对应于__parameters____args____origin__,大多数东西在打字时都会用到)。

但是it did not quite make it:

我将GenericMeta 添加到__all__,并在问题讨论之后将文档字符串添加到GenericMetaGenericMeta.__new__。 我决定不在文档字符串中描述__origin__ 和朋友。相反,我只是在第一次使用它们的地方添加了注释。

从那里开始,您仍然有三个非互斥的选项:

  • 等待typing 模块完全成熟,并希望这些功能能够尽快记录在案

  • 加入Python ideas mailing list,看看是否可以收集到足够的支持来公开这些内部结构/API 的一部分

  • 同时处理未记录的内部结构,赌注不会对这些内部进行更改,或者更改将很小。

请注意,第三点也很难避免,因为即使是API can be subject to changes

打字模块已临时包含在标准库中。可能会添加新功能,并且如果核心开发人员认为有必要,即使在次要版本之间,API 也可能会发生变化

【讨论】:

  • 我建议改写您的答案,以更加关注“__args__ 属性可以满足您的需求”。目前,您的回答主要是关于所有这些内部属性的文档和讨论,而您只是顺便提到了__args__。即使只是显示str_to_bool_dict.__args__ 输出的一行代码也能大大提高您的答案。
  • 感谢您的建议,我已经更新了答案。
【解决方案3】:

看来这个内部方法可以解决问题

typing.List[int]._subs_tree()

返回元组:

(typing.List, <class 'int'>)

但这是一个私有 API,可能有更好的答案。

【讨论】:

  • 确实,尚不清楚这是否是实现细节。依靠这个来工作似乎有点冒险。
  • 完全同意,我刚刚挖掘了这个,内部非常复杂(在 _subs_tree() 中发生了一些递归来达到这个结果)。
  • 应该指出typing 模块仍在积极(和破译)开发中。如果您希望保留此功能,我建议您提交一个用例而不是不使用它:github.com/python/typing/issues
【解决方案4】:

这个问题专门询问typing.Generic,但事实证明(至少在typing 模块的早期版本中)并非所有可下标类型都是Generic 的子类。在较新的版本中,所有可下标类型都将其参数存储在 __args__ 属性中:

>>> List[int].__args__
(<class 'int'>,)
>>> Tuple[int, str].__args__
(<class 'int'>, <class 'str'>)

然而,在 python 3.5 中,typing.Tupletyping.Uniontyping.Callable 等一些类将它们存储在不同的属性中,例如 __tuple_params____union_params__ 或通常在 __parameters__ 中。为了完整起见,这里有一个函数可以从任何 python 版本中的任何可下标类型中提取类型参数:

import typing


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _get_base_generic(cls):
        # subclasses of Generic will have their _name set to None, but
        # their __origin__ will point to the base generic
        if cls._name is None:
            return cls.__origin__
        else:
            return getattr(typing, cls._name)
else:
    # python <3.7
    def _get_base_generic(cls):
        try:
            return cls.__origin__
        except AttributeError:
            pass

        name = type(cls).__name__
        if not name.endswith('Meta'):
            raise NotImplementedError("Cannot determine base of {}".format(cls))

        name = name[:-4]
        try:
            return getattr(typing, name)
        except AttributeError:
            raise NotImplementedError("Cannot determine base of {}".format(cls))


if hasattr(typing.List, '__args__'):
    # python 3.6+
    def _get_subtypes(cls):
        subtypes = cls.__args__

        if _get_base_generic(cls) is typing.Callable:
            if len(subtypes) != 2 or subtypes[0] is not ...:
                subtypes = (subtypes[:-1], subtypes[-1])

        return subtypes
else:
    # python 3.5
    def _get_subtypes(cls):
        if isinstance(cls, typing.CallableMeta):
            if cls.__args__ is None:
                return ()

            return cls.__args__, cls.__result__

        for name in ['__parameters__', '__union_params__', '__tuple_params__']:
            try:
                subtypes = getattr(cls, name)
                break
            except AttributeError:
                pass
        else:
            raise NotImplementedError("Cannot extract subtypes from {}".format(cls))

        subtypes = [typ for typ in subtypes if not isinstance(typ, typing.TypeVar)]
        return subtypes


def get_subtypes(cls):
    """
    Given a qualified generic (like List[int] or Tuple[str, bool]) as input, return
    a tuple of all the classes listed inside the square brackets.
    """
    return _get_subtypes(cls)

演示:

>>> get_subtypes(List[int])
(<class 'int'>,)
>>> get_subtypes(Tuple[str, bool])
(<class 'str'>, <class 'bool'>)

【讨论】:

    【解决方案5】:

    在您的构造上使用.__args__。所以你需要的神奇功能是--

    get_type_args = lambda genrc_type: getattr(genrc_type, '__args__')
    

    我的问题是,如何访问这些类型参数?

    在这种情况下——我该如何访问...

    使用 Python 强大的自省功能。

    即使作为非专业程序员,我也知道我正在尝试检查东西,dir 是一个类似于终端中的 IDE 的函数。所以之后

    >>> import typing
    >>> str_to_bool_dict = typing.Dict[str, bool]
    

    我想看看有没有什么东西可以做你想要的魔法

    >>> methods = dir(str_to_bool_dict)
    >>> methods
    ['__abstractmethods__', '__args__', .....]
    

    我看到的信息太多,为了看看我是否正确,我验证了

    >>> len(methods)
    53
    >>> len(dir(dict))
    39
    

    现在让我们找到专门为泛型类型设计的方法

    >>> set(methods).difference(set(dir(dict)))
    {'__slots__', '__parameters__', '_abc_negative_cache_version', '__extra__',
    '_abc_cache', '__args__', '_abc_negative_cache', '__origin__',
    '__abstractmethods__', '__module__', '__next_in_mro__', '_abc_registry',
    '__dict__', '__weakref__'}
    

    其中,__parameters____extra____args____origin__ 听起来很有帮助。 __extra____origin__ 没有 self 将无法工作,所以我们只剩下 __parameters____args__

    >>> str_to_bool_dict.__args__
    (<class 'str'>, <class 'bool'>)
    

    这就是答案。


    Introspection 允许 py.testassert 语句使 JUnit 派生的测试框架看起来过时了。甚至像 JavaScript / Elm / Clojure 这样的语言也没有像 Python 的 dir 这样的直截了当的东西。 Python 的命名约定允许您在不实际阅读(在某些情况下例如摸索)文档的情况下发现该语言。

    因此,请使用自省并阅读文档/邮件列表来确认您的发现。

    附:致 OP——如果你不能提交邮件列表或者是一个忙碌的开发人员,这个方法还可以回答你的问题What's the correct way to check if an object is a typing.Generic? 使用发现——这就是在 python 中实现它的方法。

    【讨论】:

    • 我知道我可以启动一个交互式会话并剖析打字模块的内部结构。问题的重点是我不想这样做。我特别询问了官方记录的解决方案是什么。
    • 之前的答案已经显示了关于 __args__ 属性。这个答案似乎只是在此之上添加了不必要的华夫饼。
    猜你喜欢
    • 1970-01-01
    • 2018-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-07
    • 1970-01-01
    • 2012-01-01
    相关资源
    最近更新 更多