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

问题描述 投票:0回答:1

我正在开发一个当前支持 Python 3.6+ 的库,但在如何在 Python 3.6 的

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

这里的用例本质上是我尝试使用有效的前向引用类型定义

TypeVar
,然后我可以将其用于类型注释目的。当我使用 3.7+ 并直接从 ForwardRef 模块导入
typing
时,我已经确认以下代码
运行没有问题
,但我无法在 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 中

TypeVar
Union
的参数。

python python-typing forward-declaration
1个回答
7
投票

显而易见的是,这里的问题似乎是由于 Python 3.6 和 Python 3.7 之间

typing
模块的一些变化造成的。


在 Python 3.6 和 Python 3.7 中:

  • 在允许实例化

    TypeVar
    之前,使用 typing._type_check 函数
    (链接到 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 中:

  • _ForwardRef

    已被替换为
    a ForwardRef
    :此类不再是实现细节;它现在是模块公共 API 的一部分。

  • typing._type_check

    现在

    明确说明了
    可能将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 现在已经有点过时了,并且从 2021 年 12 月起将“正式不再受支持”,因此目前不值得花精力支持 Python 3.6。但是,如果您确实想要继续支持 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 等肯定不会

期望
你做这样的事情,并且可能支持每个版本的 Python 的
TypeVar

硬编码。


© www.soinside.com 2019 - 2024. All rights reserved.