如何在 Python 3.6 中将 ForwardRef 作为参数传递给 TypeVar?

rv.*_*tch 7 python type-hinting forward-declaration python-3.x python-typing

我正在开发一个当前支持Python 3.6+ 的库,但在如何在 Python 3.6 的模块中定义前向引用方面遇到了一些麻烦typing。我已经pyenv在本地 Windows 计算机上进行了设置,以便可以轻松地在不同的 Python 版本之间切换以进行本地测试,因为我的系统解释器默认为 Python 3.9。

这里的用例本质上是我尝试使用TypeVar有效的前向引用类型定义 a ,然后我可以将其用于类型注释目的。我已经确认当我在 3.7+ 上并直接从模块导入时,以下代码运行没有问题,但我无法在 Python 3.6 上获取它,因为我注意到前向引用不能用作某些参数原因。我还尝试将前向引用类型作为参数传递给,但遇到了类似的问题。ForwardReftypingTypeVarUnion

TypeVar以下是我试图在 python 3.6.0 以及更新版本(如 3.6.8)上工作的导入和定义- 我确实注意到我在次要版本之间遇到了不同的错误:

from typing import _ForwardRef as PyForwardRef, TypeVar


# Errors on PY 3.6:
#   3.6.2+ -> AttributeError: type object '_ForwardRef' has no attribute '_gorg'
#   3.6.2 or earlier -> AssertionError: assert isinstance(a, GenericMeta)
FREF = TypeVar('FREF', str, PyForwardRef)
Run Code Online (Sandbox Code Playgroud)

下面是我已经测试过的示例用法,它似乎按照 Python 3.7+ 的预期进行了类型检查:

class MyClass: ...


def my_func(typ: FREF):
    pass


# Type checks
my_func('testing')
my_func(PyForwardRef('MyClass'))

# Does not type check
my_func(23)
my_func(MyClass)
Run Code Online (Sandbox Code Playgroud)

到目前为止我做了什么

这是我当前用于支持 Python 3.6 的解决方法。这不太漂亮,但似乎至少可以让代码运行而不会出现任何错误。然而,这似乎并没有按预期进行类型检查——至少在 Pycharm 中是这样。

import typing

# This is needed to avoid an`AttributeError` when using PyForwardRef
# as an argument to `TypeVar`, as we do below.
if hasattr(typing, '_gorg'):  # Python 3.6.2 or lower
    _gorg = typing._gorg
    typing._gorg = lambda a: None if a is PyForwardRef else _gorg(a)
else:  # Python 3.6.3+
    PyForwardRef._gorg = None
Run Code Online (Sandbox Code Playgroud)

想知道我是否走在正确的轨道上,或者是否有一个更简单的解决方案可以用来支持 ForwardRef 类型作为 Python 3.6 的参数TypeVar或Union在 Python 3.6 中。

Ale*_*ood 7

显而易见的是,这里的问题似乎是由于typingPython 3.6 和 Python 3.7 之间模块的一些变化造成的。

\n
\n

在 Python 3.6 和 Python 3.7 中:

\n
    \n
  • 在 a 上的所有约束都使用该 函数TypeVar 进行检查(链接到 GitHub 上源代码的 3.6 分支)typing._type_checkTypeVar在允许实例化

    \n
    \n

    TypeVar.__init__在 3.6 分支中看起来像这样:

    \n
    class TypeVar(_TypingBase, _root=True):\n\n    # <-- several lines skipped -->\n\n    def __init__(self, name, *constraints, bound=None,\n             covariant=False, contravariant=False):\n\n        # <-- several lines skipped -->\n\n        if constraints and bound is not None:\n            raise TypeError("Constraints cannot be combined with bound=...")\n        if constraints and len(constraints) == 1:\n            raise TypeError("A single constraint is not allowed")\n        msg = "TypeVar(name, constraint, ...): constraints must be types."\n        self.__constraints__ = tuple(_type_check(t, msg) for t in constraints)\n\n        # etc.\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
\n

在 Python 3.6 中:

\n
    \n
  • 有一门课叫_ForwardRef. 此类的名称带有前导下划线,以警告用户它是模块的实现细节,因此该类的 API 可能会在 Python 版本之间发生意外更改。
  • \n
  • 看来typing._type_check 没有考虑到可能传递给它的可能性_ForwardRef,因此奇怪的AttributeError: type object \'_ForwardRef\' has no attribute \'_gorg\',因此出现了奇怪的错误消息。我认为没有考虑到这种可能性,因为假设用户知道不使用标记为实现细节的类。
  • \n
\n

在 Python 3.7 中:

\n
    \n
  • _ForwardRef 已被替换为类ForwardRef:该类不再是实现细节它现在是模块公共 API 的一部分。

    \n
  • \n
  • typing._type_check现在明确地解释了这种可能性ForwardRef可能传递给它的

    \n
    def _type_check(arg, msg, is_argument=True):\n    """Check that the argument is a type, and return it (internal helper).\n    As a special case, accept None and return type(None) instead. Also wrap strings\n    into ForwardRef instances. Consider several corner cases, for example plain\n    special forms like Union are not valid, while Union[int, str] is OK, etc.\n    The msg argument is a human-readable error message, e.g::\n        "Union[arg, ...]: arg should be a type."\n    We append the repr() of the actual value (truncated to 100 chars).\n    """\n\n    # <-- several lines skipped -->\n\n    if isinstance(arg, (type, TypeVar, ForwardRef)):\n        return arg\n\n    # etc.\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
\n

解决方案

\n

我很想说,鉴于 Python 3.6 现在已经有点过时了,并且从 2021 年 12 月起将不再受正式支持,因此目前不值得花精力支持 Python 3.6。但是,如果您确实想要为了继续支持Python 3.6,一个稍微干净的解决方案可能是猴子补丁typing._type_check而不是猴子补丁_ForwardRef。(我所说的“更干净”是指“更接近于解决问题的根源,而不是问题的症状”\xe2\x80\x94,它显然比现有的解决方案更简洁。)

\n
import sys \nfrom typing import TypeVar\n\nif sys.version_info < (3, 7):\n    import typing\n    from typing import _ForwardRef as PyForwardRef\n    from functools import wraps\n\n    _old_type_check = typing._type_check\n\n    @wraps(_old_type_check)\n    def _new_type_check(arg, message):\n        if arg is PyForwardRef:\n            return arg\n        return _old_type_check(arg, message)\n\n    typing._type_check = _new_type_check\n    # ensure the global namespace is the same for users\n    # regardless of the version of Python they\'re using\n    del _old_type_check, _new_type_check, typing, wraps\nelse:\n    from typing import ForwardRef as PyForwardRef\n
Run Code Online (Sandbox Code Playgroud)\n

然而,虽然这种事情作为运行时解决方案工作得很好,但老实说,我不知道是否有办法让类型检查器对这种猴子修补感到满意。Pycharm、MyPy 等肯定不会期望你做这样的事情,并且可能得到他们的支持TypeVar为每个版本的 Python 提供了对 s 的硬编码支持。

\n