typing
模块主要是为了提高代码可读性和代码文档目的。
在尝试过它并阅读了有关该模块的内容后,我成功地对它感到困惑。
即使这两个变量未初始化,下面的代码也可以工作(就像您通常初始化它们一样,例如
a = "test"
)。
我只在上面添加了类型提示,一切看起来都很好。也就是说,我没有得到
NameError
,就像我的代码中只有 a
时那样 NameError: name 'a' is not defined
以这种方式(带有类型提示)声明变量是一种好的做法吗?为什么这个有效?
from typing import Any
test_var: int
a: Any
print('hi')
我期望
test_var: int
返回一个错误,指出 test_var
未启动,我必须执行类似 test_var: int = 0
之类的操作(或任何值)。是否因为我向其中添加了类型提示而将其设置为默认值?
当您考虑所涉及的名称空间时,这相当简单。当您实际尝试使用 NameError
执行任何操作(例如将其传递给函数(如
test_var
))时,您会得到 print
,这一事实暗示了这一点。它告诉您口译员不知道您使用的name。
当您第一次为模块的全局命名空间中的变量赋值时,会发生什么情况,它会被添加到该模块的 globals 字典中,键是变量名称,值是它的值价值。您可以通过调用该模块中内置的
globals
函数来查看该字典:
pprint
以使输出更易于阅读。)
from pprint import pprint
a = 1
pprint(globals())
输出看起来像这样:
{'__annotations__': {},
...
'__name__': '__main__',
...
'a': 1,
...}
当您仔细查看该词典时,您会发现其中另一个有趣的键,即
__annotations__
。现在,它的值是一个空字典。但我打赌你已经可以猜到,如果我们用类型注释我们的变量,会发生什么:
from pprint import pprint
a: int = 1
pprint(globals())
输出:
{'__annotations__': {'a': <class 'int'>},
...
'a': 1,
...}
当我们向变量添加类型提示(即注释)时,解释器会将该名称和类型添加到相关的
__annotations__
字典中(请参阅注释赋值文档);在本例中是我们的模块。顺便说一句,由于 __annotations__
字典位于我们的全局命名空间中,我们可以直接访问它:
a: int = 1
print("a" in globals()) # True
print("a" in __annotations__) # True
最后,如果我们只是注释而不给变量赋值,会发生什么?
a: int
print("a" in globals()) # False
print("a" in __annotations__) # True
这就是为什么我们会收到错误的解释,如果我们尝试例如在此示例中打印出
a
,但否则不会出现任何错误。该代码只是告诉解释器(以及任何静态类型检查器)有关注释的信息,但它没有分配任何值,因此不会在全局命名空间字典中创建条目。
如果您考虑一下,这是有道理的:在该命名空间中,应该设置为
a
的值是什么?它没有任何价值(甚至没有 None
或 NotImplemented
或类似的东西)。对于解释器来说,a: int
行仅意味着在我们模块的__annotations__
中创建一个条目,这是完全有效的。
我还想强调这样一个事实,即正如一些人经常声称的那样,注释对于解释器以及运行时来说“并非毫无意义”。诚然,它“很少”使用,但正如我们刚刚在示例中看到的那样,您绝对可以在运行时使用注释。这是否有用显然取决于您。一些软件包,如 Pydantic 或标准库的 dataclasses
实际上严重
依赖于注释来实现其目的。在我们的示例中
__annotations__
字典中设置的值实际上是对
int
类的引用。因此,如果我们愿意的话,我们绝对可以在运行时使用它:
a: int
a_type = __annotations__["a"]
print(a_type is int) # True
print(a_type("2")) # 2
您也可以在 类命名空间中尝试这个概念(不仅仅是模块命名空间),但我将把它作为练习留给读者。
总而言之,对于要添加到任何名称空间的名称,必须为其分配一个值。 不分配值并仅提供注释就可以在该命名空间的
__annotations__
中创建条目。
Python 不会自动初始化变量,因此该变量不会被设置为任何内容。a: int