如何让 Python 数据类 InitVar 字段与 typing.get_type_hints 一起使用,同时还使用注释?
How do I get Python dataclass InitVar fields to work with typing.get_type_hints while also using annotations?
在处理 Python 数据类时,我 运行 遇到了这个很容易重现的奇怪错误。
from __future__ import annotations
import dataclasses as dc
import typing
@dc.dataclass
class Test:
foo: dc.InitVar[int]
print(typing.get_type_hints(Test))
运行 这将为您带来以下内容:
Traceback (most recent call last):
File "test.py", line 11, in <module>
print(typing.get_type_hints(Test))
File "C:\Program Files\Python310\lib\typing.py", line 1804, in get_type_hints
value = _eval_type(value, base_globals, base_locals)
File "C:\Program Files\Python310\lib\typing.py", line 324, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "C:\Program Files\Python310\lib\typing.py", line 687, in _evaluate
type_ =_type_check(
File "C:\Program Files\Python310\lib\typing.py", line 173, in _type_check
raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Forward references must evaluate to types. Got dataclasses.InitVar[int].
没有from __future__ import annotations
,似乎工作正常;但在实际代码中,我在几个不同类型的提示中使用了该导入。有没有办法让注释导入不破坏这个?
所以我实际上能够在我的 Python 3.10 环境中复制完全相同的行为,坦率地说,我对能够这样做感到有点惊讶。至少从表面上看,这个问题似乎与 InitVar
以及 typing.get_type_hints
如何解决此类非泛型类型有关。
无论如何,在我们深入了解杂草之前,有必要澄清一下 from __future__ import annotations
的工作原理。您可以在将它引入野外的 PEP 中阅读更多相关信息,但本质上“简而言之”的故事是 __future__
导入将使用它的模块中的所有注释转换为 forward-declared 注释,即用单引号包裹的注释 '
以将所有类型注释呈现为字符串值。
然后将所有类型注释转换为字符串,typing.get_type_hints
实际做的是解析那些 ForwardRef
类型——本质上是 typing库识别包装在字符串中的注释的方法——使用 class 或模块的 globals
命名空间,以及可选的 locals
命名空间(如果提供)。
这里有一个简单的例子,基本上可以概括上面讨论的所有内容。我在这里所做的不是在模块顶部使用 from __future__ import annotations
,而是手动进入并通过将所有注释包装在字符串中来转发声明所有注释。值得注意的是,这与上面问题中的表现基本相同。
import typing
from dataclasses import dataclass, InitVar
@dataclass
class Test:
foo: 'InitVar[int]'
print(typing.get_type_hints(Test))
如果好奇,您也可以尝试使用 __future__
导入,而不是手动向前声明注释,然后检查 Test.__annotations__
对象以确认最终结果与我的方式相同已经在上面定义了它。
在任何一种情况下,我们 运行 都会遇到以下相同的错误,也如上面的 OP 所述:
Traceback (most recent call last):
print(typing.get_type_hints(Test))
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 1804, in get_type_hints
value = _eval_type(value, base_globals, base_locals)
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 324, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 687, in _evaluate
type_ =_type_check(
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 173, in _type_check
raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Forward references must evaluate to types. Got dataclasses.InitVar[int].
让我们记下堆栈跟踪,因为它肯定有助于了解哪里出了问题。然而,我们可能想要探索 为什么 dataclasses.InitVar
用法首先导致了这个奇怪和不寻常的错误,这实际上是我们将要研究的从.
开始
那么 dataclasses.InitVar
怎么了?
这里的 TL;DR 下标 dataclasses.InitVar
用法有问题。无论如何,让我们只看 InitVar
在 Python 3.10:
中如何定义的相关部分
class InitVar:
def __init__(self, type):
self.type = type
def __class_getitem__(cls, type):
return InitVar(type)
请注意,__class_getitem__
是我们在注释中下标 class 时调用的方法,例如 InitVar[str]
。这调用 InitVar.__class_getitem__(str)
其中 returns InitVar(str)
.
所以这里的实际问题是,下标 InitVar[int]
用法 return 是一个 InitVar 对象,而不是基础类型,即 InitVar class 本身。
所以 typing.get_type_hints
在这里导致错误,因为它在已解析的类型注释中看到一个 InitVar
实例,而不是 InitVar
class 本身,这是一个有效类型,因为它本质上是 Python class。
嗯...但是解决这个问题最直接的方法是什么?
解决方案的(拼凑)之路
如果你至少在 Python 3.10 中查看 typing.get_type_hints
的源代码,你会注意到它将所有字符串注释显式转换为 ForwardRef
对象,然后调用ForwardRef._evaluate
每一个:
for name, value in ann.items():
...
if isinstance(value, str):
value = ForwardRef(value, is_argument=False)
>> value = _eval_type(value, base_globals, base_locals)
ForwardRef._evaluate
方法所做的是 eval
使用 class 或模块全局变量包含的引用,然后在内部调用 typing._type_check
检查包含在ForwardRef
对象。这会做一些事情,比如验证引用是来自 typing
模块的泛型类型,这里绝对不感兴趣,因为 InitVar
被明确定义为非泛型类型,在至少在 3.10.
typing._type_check
的相关位如下所示:
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
raise TypeError(f"Plain {arg} is not valid as type argument")
if isinstance(arg, (type, TypeVar, ForwardRef, types.UnionType, ParamSpec)):
return arg
if not callable(arg):
>> raise TypeError(f"{msg} Got {arg!r:.100}.")
这是上面显示的最后一行,raise TypeError(...)
这似乎是 return 我们 运行 进入的错误消息。如果您检查 _type_check
函数检查的最后一个条件,您可以猜测我们如何在我们的案例中实施最简单的解决方法:
if not callable(arg):
如果我们稍微浏览一下内置 callable
的文档,我们会得到我们可以使用的可能解决方案的第一个具体提示:
def callable(i_e_, some_kind_of_function): # real signature unknown; restored from __doc__
"""
Return whether the object is callable (i.e., some kind of function).
Note that classes are callable, as are instances of classes with a
__call__() method.
"""
所以,简单来说,我们只需要在dataclasses.InitVar
class下定义一个__call__
方法即可。这可以是一个存根方法,本质上是一个空操作,但至少 class 必须定义这个方法,以便它可以被认为是可调用的,因此 typing
模块可以接受它作为一个ForwardRef
对象中的有效引用类型。
最后,这是与 OP 中相同的示例,但稍作修改以添加一个新行,该行修补 dataclasses.InitVar
以添加必要的方法,作为存根:
from __future__ import annotations
import typing
from dataclasses import dataclass, InitVar
@dataclass
class Test:
foo: InitVar[int]
# can also be defined as:
# setattr(InitVar, '__call__', lambda *args: None)
InitVar.__call__ = lambda *args: None
print(typing.get_type_hints(Test))
该示例现在似乎按预期工作,在向前声明任何带下标的 InitVar
注释时,typing.get_type_hints
方法没有引发任何错误。
在处理 Python 数据类时,我 运行 遇到了这个很容易重现的奇怪错误。
from __future__ import annotations
import dataclasses as dc
import typing
@dc.dataclass
class Test:
foo: dc.InitVar[int]
print(typing.get_type_hints(Test))
运行 这将为您带来以下内容:
Traceback (most recent call last):
File "test.py", line 11, in <module>
print(typing.get_type_hints(Test))
File "C:\Program Files\Python310\lib\typing.py", line 1804, in get_type_hints
value = _eval_type(value, base_globals, base_locals)
File "C:\Program Files\Python310\lib\typing.py", line 324, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "C:\Program Files\Python310\lib\typing.py", line 687, in _evaluate
type_ =_type_check(
File "C:\Program Files\Python310\lib\typing.py", line 173, in _type_check
raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Forward references must evaluate to types. Got dataclasses.InitVar[int].
没有from __future__ import annotations
,似乎工作正常;但在实际代码中,我在几个不同类型的提示中使用了该导入。有没有办法让注释导入不破坏这个?
所以我实际上能够在我的 Python 3.10 环境中复制完全相同的行为,坦率地说,我对能够这样做感到有点惊讶。至少从表面上看,这个问题似乎与 InitVar
以及 typing.get_type_hints
如何解决此类非泛型类型有关。
无论如何,在我们深入了解杂草之前,有必要澄清一下 from __future__ import annotations
的工作原理。您可以在将它引入野外的 PEP 中阅读更多相关信息,但本质上“简而言之”的故事是 __future__
导入将使用它的模块中的所有注释转换为 forward-declared 注释,即用单引号包裹的注释 '
以将所有类型注释呈现为字符串值。
然后将所有类型注释转换为字符串,typing.get_type_hints
实际做的是解析那些 ForwardRef
类型——本质上是 typing库识别包装在字符串中的注释的方法——使用 class 或模块的 globals
命名空间,以及可选的 locals
命名空间(如果提供)。
这里有一个简单的例子,基本上可以概括上面讨论的所有内容。我在这里所做的不是在模块顶部使用 from __future__ import annotations
,而是手动进入并通过将所有注释包装在字符串中来转发声明所有注释。值得注意的是,这与上面问题中的表现基本相同。
import typing
from dataclasses import dataclass, InitVar
@dataclass
class Test:
foo: 'InitVar[int]'
print(typing.get_type_hints(Test))
如果好奇,您也可以尝试使用 __future__
导入,而不是手动向前声明注释,然后检查 Test.__annotations__
对象以确认最终结果与我的方式相同已经在上面定义了它。
在任何一种情况下,我们 运行 都会遇到以下相同的错误,也如上面的 OP 所述:
Traceback (most recent call last):
print(typing.get_type_hints(Test))
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 1804, in get_type_hints
value = _eval_type(value, base_globals, base_locals)
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 324, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 687, in _evaluate
type_ =_type_check(
File "C:\Users\USER\.pyenv\pyenv-win\versions.10.0\lib\typing.py", line 173, in _type_check
raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Forward references must evaluate to types. Got dataclasses.InitVar[int].
让我们记下堆栈跟踪,因为它肯定有助于了解哪里出了问题。然而,我们可能想要探索 为什么 dataclasses.InitVar
用法首先导致了这个奇怪和不寻常的错误,这实际上是我们将要研究的从.
那么 dataclasses.InitVar
怎么了?
这里的 TL;DR 下标 dataclasses.InitVar
用法有问题。无论如何,让我们只看 InitVar
在 Python 3.10:
class InitVar:
def __init__(self, type):
self.type = type
def __class_getitem__(cls, type):
return InitVar(type)
请注意,__class_getitem__
是我们在注释中下标 class 时调用的方法,例如 InitVar[str]
。这调用 InitVar.__class_getitem__(str)
其中 returns InitVar(str)
.
所以这里的实际问题是,下标 InitVar[int]
用法 return 是一个 InitVar 对象,而不是基础类型,即 InitVar class 本身。
所以 typing.get_type_hints
在这里导致错误,因为它在已解析的类型注释中看到一个 InitVar
实例,而不是 InitVar
class 本身,这是一个有效类型,因为它本质上是 Python class。
嗯...但是解决这个问题最直接的方法是什么?
解决方案的(拼凑)之路
如果你至少在 Python 3.10 中查看 typing.get_type_hints
的源代码,你会注意到它将所有字符串注释显式转换为 ForwardRef
对象,然后调用ForwardRef._evaluate
每一个:
for name, value in ann.items():
...
if isinstance(value, str):
value = ForwardRef(value, is_argument=False)
>> value = _eval_type(value, base_globals, base_locals)
ForwardRef._evaluate
方法所做的是 eval
使用 class 或模块全局变量包含的引用,然后在内部调用 typing._type_check
检查包含在ForwardRef
对象。这会做一些事情,比如验证引用是来自 typing
模块的泛型类型,这里绝对不感兴趣,因为 InitVar
被明确定义为非泛型类型,在至少在 3.10.
typing._type_check
的相关位如下所示:
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
raise TypeError(f"Plain {arg} is not valid as type argument")
if isinstance(arg, (type, TypeVar, ForwardRef, types.UnionType, ParamSpec)):
return arg
if not callable(arg):
>> raise TypeError(f"{msg} Got {arg!r:.100}.")
这是上面显示的最后一行,raise TypeError(...)
这似乎是 return 我们 运行 进入的错误消息。如果您检查 _type_check
函数检查的最后一个条件,您可以猜测我们如何在我们的案例中实施最简单的解决方法:
if not callable(arg):
如果我们稍微浏览一下内置 callable
的文档,我们会得到我们可以使用的可能解决方案的第一个具体提示:
def callable(i_e_, some_kind_of_function): # real signature unknown; restored from __doc__
"""
Return whether the object is callable (i.e., some kind of function).
Note that classes are callable, as are instances of classes with a
__call__() method.
"""
所以,简单来说,我们只需要在dataclasses.InitVar
class下定义一个__call__
方法即可。这可以是一个存根方法,本质上是一个空操作,但至少 class 必须定义这个方法,以便它可以被认为是可调用的,因此 typing
模块可以接受它作为一个ForwardRef
对象中的有效引用类型。
最后,这是与 OP 中相同的示例,但稍作修改以添加一个新行,该行修补 dataclasses.InitVar
以添加必要的方法,作为存根:
from __future__ import annotations
import typing
from dataclasses import dataclass, InitVar
@dataclass
class Test:
foo: InitVar[int]
# can also be defined as:
# setattr(InitVar, '__call__', lambda *args: None)
InitVar.__call__ = lambda *args: None
print(typing.get_type_hints(Test))
该示例现在似乎按预期工作,在向前声明任何带下标的 InitVar
注释时,typing.get_type_hints
方法没有引发任何错误。