【问题标题】:How to pass ForwardRef as args to TypeVar in Python 3.6?如何在 Python 3.6 中将 ForwardRef 作为 args 传递给 TypeVar?
【发布时间】:2021-10-03 17:14:16
【问题描述】:

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

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

【问题讨论】:

    标签: python python-3.x type-hinting forward-declaration python-typing


    【解决方案1】:

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


    在 Python 3.6 和 Python 3.7 中:

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


      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。该类的名称带有前导下划线,以警告用户这是模块的实现细节,因此该类的 API 可能会在 Python 版本之间发生意外更改。
    • 似乎typing._type_check did not account 可能会传递给它_ForwardRef,因此出现了奇怪的AttributeError: type object '_ForwardRef' has no attribute '_gorg' 错误消息。我假设没有考虑到这种可能性,因为假设用户知道不使用标记为实现细节的类。

    在 Python 3.7 中:

    • _ForwardRefhas been replaceda ForwardRef class:这个类不再是一个实现细节;它现在是模块公共 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 真的不值得,从 2021 年 12 月起将是officially unsupported。但是,如果你确实想要为了继续支持 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。

    【讨论】:

    • 精彩的解决方案和解释!我之前没有考虑过修补 typing._type_check,因为我没有仔细研究 3.6 和 3.7 之间的打字差异,但实际上装饰它以显式检查 ForwardRef 类型是有意义的。
    • 我同意你的方式看起来更干净。我试图修补 _gorg 属性,因为这似乎是错误所表明的,但是这种方法似乎更安全一些。我想我也弄清楚了为什么 Pycharm 对 3.6 中的 TypeVar 不满意——看起来那是因为我正在导入 typing._ForwardRef 并且我猜 Pycharm 正在寻找没有前导下划线的导入。如果它最终没有对 3.6 进行类型检查,这没什么大不了的 - 我同意它可能值得尽快放弃对它的支持。
    • @rv.kvetch 太棒了——很高兴我能帮上忙!
    猜你喜欢
    • 1970-01-01
    • 2020-08-09
    • 2011-04-04
    • 2014-08-22
    • 1970-01-01
    • 1970-01-01
    • 2020-01-11
    • 2021-12-01
    • 1970-01-01
    相关资源
    最近更新 更多