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

How to pass ForwardRef as args to TypeVar in Python 3.6?

我正在开发一个目前支持 Python 3.6+ 的库,但是在 [=14] 中如何定义前向引用时遇到了一些麻烦=] Python 3.6 中的模块。我在我的本地 Windows 机器上设置了 pyenv,这样我就可以在不同的 Python 版本之间轻松切换以进行本地测试,因为我的系统解释器默认为 Python 3.9。

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

这里是 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)

这是我已经能够测试的示例用法,它似乎按 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)

到目前为止我做了什么

这是我目前用来支持 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

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

显而易见的是,这里的问题似乎是由于 typing 模块在 Python 3.6 和 Python 3.7 之间发生了一些变化。


在 Python 3.6 和 Python 3.7 中:

  • TypeVar are checked using the typing._type_check function 上的所有约束(链接到 GitHub 上源代码的 3.6 分支)在 TypeVar 之前允许实例化。


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

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

在 Python 3.6:

  • a class called _ForwardRef。此 class 的名称带有前导下划线,以警告用户它是模块的实现细节,因此 class 的 API 可能会在 [=77 之间意外更改=] 版本。
  • 似乎 typing._type_check did not account 因为 _ForwardRef 可能会传递给它,因此出现了奇怪的 AttributeError: type object '_ForwardRef' has no attribute '_gorg' 错误消息。我假设没有考虑到这种可能性,因为假设用户不会使用标记为实现细节的 classes。

在 Python 3.7:

  • _ForwardRef has been replaced with a ForwardRef class:这个class不再是实现细节;它现在是模块 public API.

    的一部分
  • typing._type_check 现在 explicitly accounts 因为 ForwardRef 可能被传递给它的可能性:

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

解决方案

我很想争辩说目前不值得支持 Python 3.6,因为 Python 3.6 现在有点老了,而且会 officially unsupported 从 2021 年 12 月开始。但是,如果您确实想继续支持 Python 3.6,一个更简洁的解决方案可能是猴子补丁 typing._type_check 而不是猴子补丁 _ForwardRef。 (我所说的“更干净”是指“更接近于解决问题的根源,而不是问题的症状”——它显然不如您现有的解决方案简洁。)

import sys 
from typing import TypeVar

if sys.version_info < (3, 7):
    import typing
    from typing import _ForwardRef as PyForwardRef
    from functools import wraps

    _old_type_check = typing._type_check

    @wraps(_old_type_check)
    def _new_type_check(arg, message):
        if arg is PyForwardRef:
            return arg
        return _old_type_check(arg, message)

    typing._type_check = _new_type_check
    # ensure the global namespace is the same for users
    # regardless of the version of Python they're using
    del _old_type_check, _new_type_check, typing, wraps
else:
    from typing import ForwardRef as PyForwardRef

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