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



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

这里的用例本质上是我试图用有效的前向引用类型定义TypeVar,然后我可以将其用于类型注释目的。我确认下面的代码运行没有问题当我在3.7+上并直接从typing模块导入ForwardRef时,但我无法在Python 3.6上获得它,因为我注意到由于某种原因,转发refs不能用作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 3.6和Python 3.7之间typing模块的几个更改。


在Python 3.6和Python 3.7中:

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


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

Python 3.7:

  • _ForwardRef已被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,一个更简洁的解决方案可能是使用monkey-patchtyping._type_check而不是monkey-patch_ForwardRef。(通过"cleaner"我的意思是"更接近于解决问题的根源,而不是问题的症状"。-它显然不如你现有的解决方案简洁。)

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

不过,虽然这种事情作为一个运行时的解决方案,我真的不知道是否有一种方法使type-checkers满意这种猴子补丁。Pycharm, MyPy等当然不会期望您做这样的事情,并且可能会为每个版本的Python硬编码TypeVar支持。

最新更新