我正在开发一个库,目前支持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中TypeVar
或Union
的参数。
说明显而易见的问题,这里的问题似乎是由于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
支持。