lap*_*ken 6 python python-3.x python-dataclasses python-typing python-3.10
当处理 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))
Run Code Online (Sandbox Code Playgroud)
运行它会得到以下结果:
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].
Run Code Online (Sandbox Code Playgroud)
没有from __future__ import annotations
,它似乎工作得很好;但在实际代码中,我在几个不同类型的提示中使用了该导入。有没有办法让注释导入不会破坏这个?
rv.*_*tch 11
因此,我实际上能够在 Python 3.10 环境中复制完全相同的行为,坦率地说,我对能够做到这一点感到有点惊讶。至少从表面上看,问题似乎在于InitVar
如何typing.get_type_hints
解决此类非泛型类型。
不管怎样,在我们深入了解之前,有必要先澄清一下它的from __future__ import annotations
工作原理。您可以在将其引入野外的PEP中阅读更多相关信息,但本质上“简而言之”的故事是导入将__future__
模块中使用它的所有注释转换为前向声明的注释,即包装的注释用单引号'
将所有类型注释呈现为字符串值。
因此,将所有类型注释转换为字符串后,实际typing.get_type_hints
所做的就是ForwardRef
使用类或模块的命名空间以及可选的命名空间(如果假如。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))
Run Code Online (Sandbox Code Playgroud)
如果好奇,您还可以尝试__future__
导入而不手动向前声明注释,然后检查Test.__annotations__
对象以确认最终结果与我上面定义的方式相同。
无论哪种情况,我们都会遇到下面相同的错误,也如上面的 OP 中所述:
Traceback (most recent call last):
print(typing.get_type_hints(Test))
File "C:\Users\USER\.pyenv\pyenv-win\versions\3.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\3.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\3.10.0\lib\typing.py", line 687, in _evaluate
type_ =_type_check(
File "C:\Users\USER\.pyenv\pyenv-win\versions\3.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].
Run Code Online (Sandbox Code Playgroud)
让我们记下堆栈跟踪,因为它对于了解哪里出了问题肯定很有用。然而,我们可能首先想要探究为什么这种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)
Run Code Online (Sandbox Code Playgroud)
请注意,__class_getitem__
是当我们在注释中为类添加下标时调用的方法,例如 like InitVar[str]
。这调用了InitVar.__class_getitem__(str)
which 返回InitVar(str)
。
所以这里的实际问题是,下标InitVar[int]
用法返回一个 InitVar 对象,而不是底层类型,即 InitVar 类本身。
所以typing.get_type_hints
这里会导致错误,因为它InitVar
在解析的类型注释中看到一个实例,而不是InitVar
类本身,这是一个有效的类型,因为它本质上是一个 Python 类。
嗯...但是解决这个问题最直接的方法是什么?
typing.get_type_hints
如果您至少查看 Python 3.10 的源代码,您会注意到它将所有字符串注释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)
Run Code Online (Sandbox Code Playgroud)
该ForwardRef._evaluate
方法所做的是eval
使用类或模块全局变量包含的引用,然后在内部调用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}.")
Run Code Online (Sandbox Code Playgroud)
这是上面显示的最后一行,raise TypeError(...)
它似乎返回了我们遇到的错误消息。如果您检查该_type_check
函数检查的最后一个条件,您可以猜测我们如何在我们的案例中实现最简单的解决方法:
if not callable(arg):
Run Code Online (Sandbox Code Playgroud)
如果我们简单地浏览一下内置的文档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.
"""
Run Code Online (Sandbox Code Playgroud)
所以,简单来说,我们需要做的就是在类__call__
下定义一个方法dataclasses.InitVar
。这可以是一个存根方法,本质上是一个无操作,但至少类必须定义此方法,以便它可以被视为可调用,因此模块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))
Run Code Online (Sandbox Code Playgroud)
现在,该示例似乎按预期工作,typing.get_type_hints
在向前声明任何下标InitVar
注释时,该方法不会引发任何错误。
归档时间: |
|
查看次数: |
2133 次 |
最近记录: |