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