28 KiB
Python 中的名称空间和范围
*立即观看**本教程有真实 Python 团队创建的相关视频课程。与书面教程一起观看,加深您的理解: 在 Python 中导航名称空间和范围
本教程涵盖了 Python 名称空间,这种结构用于组织在 Python 程序中分配给对象的符号名称。
本系列之前的教程已经强调了 Python 中 对象 的重要性。对象无处不在!事实上,Python 程序创建或操作的所有东西都是对象。
一个赋值语句创建一个符号名,你可以用它来引用一个对象。语句x = 'foo'创建了一个符号名x,它引用了字符串对象'foo'。
在任何复杂的程序中,你都会创建成百上千个这样的名字,每个名字都指向一个特定的对象。Python 如何跟踪所有这些名称,使它们不会互相干扰?
在本教程中,您将学习:
- Python 如何在名称空间中组织符号名称和对象
- 当 Python 创建一个新的名称空间时
- 名称空间是如何实现的
- 变量作用域如何决定符号名的可见性
免费奖励: 掌握 Python 的 5 个想法,这是一个面向 Python 开发者的免费课程,向您展示将 Python 技能提升到下一个水平所需的路线图和心态。
Python 中的名称空间
名称空间是当前定义的符号名称以及每个名称引用的对象信息的集合。您可以将名称空间想象成一个字典,其中的键是对象名,值是对象本身。每个键值对都将一个名称映射到其对应的对象。
名称空间是一个非常棒的想法——让我们多做一些吧!
——蟒蛇的禅,作者蒂姆·皮特斯
正如 Tim Peters 所说,名称空间不仅仅是伟大的。它们很棒,Python 广泛使用它们。在 Python 程序中,有四种类型的名称空间:
- 内置的
- 全球的
- 封闭
- 当地的
这些具有不同的寿命。Python 执行程序时,会根据需要创建名称空间,并在不再需要时删除它们。通常,在任何给定时间都会存在许多名称空间。
内置名称空间
内置名称空间包含所有 Python 内置对象的名称。当 Python 运行时,这些都是可用的。您可以使用以下命令列出内置名称空间中的对象:
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError',
'BaseException','BlockingIOError', 'BrokenPipeError', 'BufferError',
'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError',
'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError',
'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError',
'Exception', 'False', 'FileExistsError', 'FileNotFoundError',
'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError',
'ImportError', 'ImportWarning', 'IndentationError', 'IndexError',
'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt',
'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError',
'OverflowError', 'PendingDeprecationWarning', 'PermissionError',
'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning',
'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration',
'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError',
'TimeoutError', 'True', 'TypeError', 'UnboundLocalError',
'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError',
'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError',
'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__',
'__doc__', '__import__', '__loader__', '__name__', '__package__',
'__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray',
'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex',
'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate',
'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct',
'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr',
'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
您将在这里看到一些您可能在以前的教程中见过的对象—例如, StopIteration 异常,内置函数,如 max() 和 len() ,以及对象类型,如int和str。
Python 解释器在启动时创建内置名称空间。这个名称空间一直存在,直到解释器终止。
全局名称空间
全局名称空间包含在主程序级别定义的任何名称。Python 在主程序体启动时创建全局名称空间,并且它一直存在,直到解释器终止。
严格地说,这可能不是唯一存在的全局名称空间。解释器还为程序用 import 语句加载的任何模块创建一个全局名称空间。要进一步了解 Python 中的主要函数和模块,请参阅以下资源:
在本系列的后续教程中,您将更详细地探索这些模块。目前,当您看到术语全局名称空间时,请考虑属于主程序的名称空间。
本地和封闭名称空间
正如您在上一篇关于函数的教程中所学的,每当函数执行时,解释器都会创建一个新的名称空间。该名称空间是函数的本地名称,并且在函数终止之前一直存在。
只有在主程序的层次上,函数才不是彼此独立存在的。您也可以在另一个中定义一个函数:
1>>> def f(): 2... print('Start f()')
3...
4... def g(): 5... print('Start g()')
6... print('End g()')
7... return 8...
9... g() 10...
11... print('End f()')
12... return 13...
14
15>>> f() 16Start f()
17Start g()
18End g()
19End f()
在这个例子中,函数g()被定义在f()的主体中。下面是这段代码中发生的情况:
- 第 1 到 12 行定义
f(),包含功能的**。** - 第 4 行到第 7 行定义
g(),其中包含功能。 - 在第 15 行,主程序调用
f()。 - 在9 号线,
f()呼叫g()。
当主程序调用f()时,Python 为f()创建一个新的名称空间。类似地,当f()调用g()时,g()获得自己独立的名称空间。为g()创建的名称空间是本地名称空间,为f()创建的名称空间是封闭名称空间。
这些名称空间中的每一个都保持存在,直到其各自的功能终止。当名称空间的函数终止时,Python 可能不会立即回收为这些名称空间分配的内存,但是对它们包含的对象的所有引用都不再有效。
可变范围
多个不同名称空间的存在意味着在 Python 程序运行时,特定名称的几个不同实例可以同时存在。只要每个实例在不同的名称空间中,它们都是单独维护的,不会互相干扰。
但是这就产生了一个问题:假设您在代码中引用了名称x,并且x存在于几个名称空间中。Python 怎么知道你说的是哪个?
答案在于范围的概念。名字的范围是该名字有意义的程序区域。解释器在运行时根据名字定义出现的位置和代码中名字被引用的位置来确定这一点。
**延伸阅读:**参见维基百科关于计算机编程中的作用域的页面了解编程语言中变量作用域的详细讨论。
如果你更喜欢钻研视频课程,那就去看看探索 Python 中的作用域和闭包或者用 Python 基础:作用域回到基础。
回到上面的问题,如果您的代码引用了名称x,那么 Python 将在下面的名称空间中按照所示的顺序搜索x:
- 局部:如果你在一个函数中引用
x,那么解释器首先在该函数局部的最内层作用域中搜索它。 - 封闭:如果
x不在局部范围内,但是出现在驻留在另一个函数内的函数中,那么解释器在封闭函数的范围内搜索。 - 全局:如果上面的搜索都没有结果,那么解释器接下来在全局范围内查找。
- 内置:如果在别的地方找不到
x,那么解释器就尝试内置作用域。
这就是 Python 文献中通常所说的 LEGB 规则(尽管这个术语实际上并没有出现在 Python 文档中)。解释器从里到外搜索一个名字,在 l ocal、 e nclosing、 g lobal、最后是 b 内置范围中查找:
如果解释器在这些位置都找不到这个名字,Python 就会抛出一个 NameError异常。
例子
下面是 LEGB 规则的几个例子。在每种情况下,最里面的封闭函数g()试图向控制台显示名为x的变量的值。注意每个例子是如何根据范围为x打印不同的值的。
示例 1:单一定义
在第一个例子中,x只在一个位置定义。它在f()和g()之外,所以驻留在全局范围内:
1>>> x = 'global' 2
3>>> def f():
4...
5... def g():
6... print(x) 7...
8... g()
9...
10
11>>> f()
12global
第 6 行的 print() 语句只能引用一个可能的x。它显示了在全局名称空间中定义的x对象,也就是字符串'global'。
示例 2:双重定义
在下一个例子中,x的定义出现在两个地方,一个在f()之外,一个在f()之内,但是在g()之外:
1>>> x = 'global' 2
3>>> def f():
4... x = 'enclosing' 5...
6... def g():
7... print(x) 8...
9... g()
10...
11
12>>> f()
13enclosing
和前面的例子一样,g()指的是x。但这一次,它有两个定义可供选择:
- 第 1 行在全局范围内定义
x。 - 第 4 行在封闭范围内再次定义了
x。
根据 LEGB 规则,解释器在查看全局范围之前从封闭范围中找到值。所以第七行的语句显示的是'enclosing'而不是'global'。
示例 3:三重定义
接下来是x在这里、那里和任何地方被定义的情况。一个定义在f()之外,另一个定义在f()之内,但在g()之外,第三个定义在g()之内:
1>>> x = 'global' 2
3>>> def f():
4... x = 'enclosing' 5...
6... def g():
7... x = 'local' 8... print(x) 9...
10... g()
11...
12
13>>> f()
14local
现在,第 8 行的语句必须区分三种不同的可能性:
- 第 1 行在全局范围内定义
x。 - 第 4 行在封闭范围内再次定义了
x。 - 第 7 行在
g()的局部范围内第三次定义了x。
这里,LEGB 规则规定g()首先看到自己本地定义的值x。所以print()语句显示'local'。
示例 4:无定义
最后,我们有一个例子,其中g()试图打印x的值,但是x在任何地方都没有定义。那根本行不通:
1>>> def f():
2...
3... def g():
4... print(x) 5...
6... g()
7...
8
9>>> f()
10Traceback (most recent call last):
11 File "<stdin>", line 1, in <module>
12 File "<stdin>", line 6, in f
13 File "<stdin>", line 4, in g
14NameError: name 'x' is not defined
这一次,Python 没有在任何名称空间中找到x,所以第 4 行的语句生成了一个NameError异常。
Python 名称空间词典
在本教程的前面,当第一次引入名称空间时,我们鼓励您将名称空间看作一个字典,其中的键是对象名,值是对象本身。事实上,对于全局和局部命名空间来说,这正是它们的意义所在!Python 确实将这些名称空间实现为字典。
**注意:**内置名称空间的行为不像字典。Python 将其实现为一个模块。
Python 提供了名为globals()和locals()的内置函数,允许您访问全局和本地名称空间字典。
globals()功能
内置函数globals()返回对当前全局名称空间字典的引用。您可以使用它来访问全局名称空间中的对象。下面是主程序启动时的一个示例:
>>> type(globals())
<class 'dict'>
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
如您所见,解释器已经在globals()中放入了几个条目。根据您的 Python 版本和操作系统,它在您的环境中可能会有所不同。不过应该差不多。
现在看看在全局范围内定义变量时会发生什么:
>>> x = 'foo'
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'x': 'foo'}
在赋值语句x = 'foo'之后,一个新的条目出现在全局名称空间字典中。字典键是对象的名称x,字典值是对象的值'foo'。
您通常会以通常的方式访问这个对象,通过引用它的符号名x。但是您也可以通过全局名称空间字典间接访问它:
1>>> x
2'foo'
3>>> globals()['x']
4'foo'
5
6>>> x is globals()['x'] 7True
第六行的上的 is比较确认这些实际上是同一物体。
您也可以使用globals()函数在全局名称空间中创建和修改条目:
1>>> globals()['y'] = 100 2
3>>> globals()
4{'__name__': '__main__', '__doc__': None, '__package__': None,
5'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
6'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
7'x': 'foo', 'y': 100} 8
9>>> y
10100 11
12>>> globals()['y'] = 3.14159 13
14>>> y 153.14159
第 1 行的语句与赋值语句y = 100具有同等效力。第 12 行上的语句等同于y = 3.14159。
当简单的赋值语句就可以在全局范围内创建和修改对象时,这种方式有点偏离常规。但是它是有效的,它很好地解释了这个概念。
locals()功能
Python 也提供了相应的内置函数,名为locals()。它类似于globals(),但是访问本地名称空间中的对象:
>>> def f(x, y):
... s = 'foo'
... print(locals())
...
>>> f(10, 0.5)
{'s': 'foo', 'y': 0.5, 'x': 10}
当在f()中调用时,locals()返回一个表示函数的本地名称空间的字典。注意,除了本地定义的变量s,本地名称空间还包括函数参数x和y,因为它们对于f()也是本地的。
如果您在主程序中的函数外部调用locals(),那么它的行为与globals()相同。
深度潜水:
globals()和locals()之间的细微差别了解一下
globals()和locals()之间的一个小区别是很有用的。
globals()返回包含全局名称空间的字典的实际引用。这意味着如果您调用globals(),保存返回值,并随后定义额外的变量,那么这些新变量将出现在保存的返回值所指向的字典中:`1>>> g = globals() 2>>> g 3{'__name__': '__main__', '__doc__': None, '__package__': None, 4'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, 5'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 6'g': {...}} 7 8>>> x = 'foo' 9>>> y = 29 10>>> g 11{'__name__': '__main__', '__doc__': None, '__package__': None, 12'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, 13'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 14'g': {...}, 'x': 'foo', 'y': 29}`这里,
g是对全局名称空间字典的引用。在第 8 行和第 9 行的赋值语句之后,x和y出现在g指向的字典中。另一方面,
locals()返回的字典是本地名称空间的当前副本,而不是对它的引用。对本地名称空间的进一步添加不会影响之前从locals()返回的值,直到您再次调用它。此外,您不能使用来自locals()的返回值来修改实际本地名称空间中的对象:`1>>> def f(): 2... s = 'foo' 3... loc = locals() 4... print(loc) 5... 6... x = 20 7... print(loc) 8... 9... loc['s'] = 'bar' 10... print(s) 11... 12 13>>> f() 14{'s': 'foo'} 15{'s': 'foo'} 16foo`在这个例子中,
loc指向来自locals()的返回值,它是本地名称空间的副本。第 6 行第行的语句x = 20将x添加到本地名称空间,但不会将添加到loc指向的副本。类似地,第 9 行上的语句修改了loc指向的副本中键's'的值,但这对实际本地名称空间中的s的值没有影响。这是一个微妙的区别,但如果你不记得它,它可能会给你带来麻烦。
修改超出范围的变量
在本系列的早些时候,在关于用户定义的 Python 函数的教程中,您了解到 Python 中的参数传递有点像按值传递,有点像按引用传递。有时,函数可以通过更改相应的参数来修改其在调用环境中的参数,有时则不能:
- 不可变的参数永远不能被函数修改。
- 一个可变的参数不能被大规模地重新定义,但是它可以被适当地修改。
**注:**关于修改函数参数的更多信息,请参见 Pascal 中的按值传递 vs 按引用传递和 Python 中的按值传递 vs 按引用传递。
当函数试图修改其局部范围之外的变量时,也存在类似的情况。一个函数根本不能在它的局部范围之外修改一个不可变的对象:
1>>> x = 20
2>>> def f():
3... x = 40 4... print(x)
5...
6
7>>> f()
840
9>>> x
1020
当f()执行行 3 上的赋值x = 40时,它创建一个新的本地引用到一个值为40的整数对象。此时,f()失去了对全局名称空间中名为x的对象的引用。所以赋值语句不会影响全局对象。
注意当f()在行 4 执行print(x)时,显示40,它自己的局部x的值。但是f()终止后,全局范围内的x仍然是20。
如果函数在适当的位置修改了可变类型的对象,那么它可以在局部范围之外修改该对象:
>>> my_list = ['foo', 'bar', 'baz']
>>> def f():
... my_list[1] = 'quux' ...
>>> f()
>>> my_list
['foo', 'quux', 'baz']
在这种情况下,my_list是一个列表,列表是可变的。f()可以在my_list内进行修改,即使它不在本地范围内。
但是如果f()试图完全重新分配my_list,那么它将创建一个新的本地对象,而不会修改全局my_list:
>>> my_list = ['foo', 'bar', 'baz']
>>> def f():
... my_list = ['qux', 'quux'] ...
>>> f()
>>> my_list
['foo', 'bar', 'baz']
这类似于当f()试图修改可变函数参数时发生的情况。
global声明
如果您确实需要从f()内部修改全局范围内的值,该怎么办?在 Python 中使用global声明可以做到这一点:
>>> x = 20
>>> def f():
... global x ... x = 40 ... print(x)
...
>>> f()
40
>>> x
40
global x语句表明当f()执行时,对名字x的引用将指向全局名称空间中的x。这意味着赋值x = 40不会创建新的引用。而是在全局范围内给x赋一个新值:
正如您已经看到的,globals()返回对全局名称空间字典的引用。如果您愿意,可以不使用global语句,而是使用globals()来完成同样的事情:
>>> x = 20
>>> def f():
... globals()['x'] = 40 ... print(x)
...
>>> f()
40
>>> x 40
没有太多的理由这样做,因为global声明可以说使意图更加清晰。但它确实为globals()如何工作提供了另一个例证。
如果函数启动时在global声明中指定的名字在全局范围内不存在,那么global语句和赋值的组合将创建它:
1>>> y
2Traceback (most recent call last):
3 File "<pyshell#79>", line 1, in <module>
4 y
5NameError: name 'y' is not defined
6
7>>> def g():
8... global y
9... y = 20
10...
11
12>>> g()
13>>> y
1420
在这种情况下,当g()启动时,全局范围内没有名为y的对象,但是g()在的第 8 行用global y语句创建了一个对象。
您也可以在单个global声明中指定几个逗号分隔的名称:
1>>> x, y, z = 10, 20, 30
2
3>>> def f():
4... global x, y, z
5...
这里,x、y、z都是通过第 4 行的单个global语句声明引用全局范围内的对象。
在global声明中指定的名称不能出现在global语句之前的函数中:
1>>> def f():
2... print(x)
3... global x
4...
5 File "<stdin>", line 3
6SyntaxError: name 'x' is used prior to global declaration
第 3 行上的global x语句的目的是使对x的引用指向全局范围内的一个对象。但是行 2** 的print()声明是指x到global声明之前。这引发了一个 SyntaxError 异常。**
nonlocal声明
嵌套函数定义也存在类似的情况。global声明允许函数在全局范围内访问和修改对象。如果被封闭的函数需要修改封闭范围内的对象怎么办?考虑这个例子:
1>>> def f():
2... x = 20
3...
4... def g():
5... x = 40 6...
7... g()
8... print(x)
9...
10
11>>> f()
1220
在这种情况下,x的第一个定义是在封闭范围内,而不是在全局范围内。正如g()不能在全局范围内直接修改变量一样,它也不能在封闭函数的范围内修改x。在行 5 赋值x = 40后,包围范围内的x保留20。
global关键词不是这种情况的解决方案:
>>> def f():
... x = 20
...
... def g():
... global x ... x = 40 ...
... g()
... print(x)
...
>>> f()
20
因为x在封闭函数的范围内,而不是全局范围内,所以global关键字在这里不起作用。g()终止后,包围范围内的x仍然是20。
事实上,在这个例子中,global x语句不仅不能在封闭范围内提供对x的访问,而且还在全局范围内创建了一个名为x的对象,其值为40:
>>> def f():
... x = 20
...
... def g():
... global x
... x = 40
...
... g()
... print(x)
...
>>> f()
20
>>> x 40
要从g()内部修改封闭范围内的x,需要类似的关键字 nonlocal 。在nonlocal关键字后指定的名称指最近的封闭范围内的变量:
1>>> def f():
2... x = 20
3...
4... def g():
5... nonlocal x 6... x = 40 7...
8... g()
9... print(x)
10...
11
12>>> f()
1340
在行 5 的nonlocal x语句后,当g()指x时,指最近的包围范围内的x,其定义在行 2 的f()中:
第 9 行的f()末尾的print()语句确认对g()的调用已经将封闭范围内的x的值更改为40。
最佳实践
尽管 Python 提供了global和nonlocal关键字,但使用它们并不总是明智的。
当一个函数在局部范围之外修改数据时,无论是使用global还是nonlocal关键字,或者直接修改一个可变类型,这都是一种副作用,类似于函数修改它的一个参数。广泛修改全局变量通常被认为是不明智的,不仅在 Python 中如此,在其他编程语言中也是如此。
和许多事情一样,这在某种程度上是风格和偏好的问题。有时候,明智地使用全局变量修改可以降低程序的复杂性。
在 Python 中,使用global关键字至少可以清楚地表明函数正在修改一个全局变量。在许多语言中,函数可以通过赋值来修改全局变量,而不用以任何方式声明它。这使得跟踪全局数据被修改的位置变得非常困难。
总而言之,在局部范围之外修改变量通常是不必要的。几乎总有更好的方法,通常是函数返回值。
结论
Python 程序使用或作用的几乎所有东西都是对象。即使是很短的程序也会创建许多不同的对象。在一个更复杂的程序中,它们可能会数以千计。Python 必须跟踪所有这些对象和它们的名字,它用名称空间来做这件事。
在本教程中,您学习了:
- Python 中有哪些不同的名称空间
- 当 Python 创建一个新的名称空间时
- Python 使用什么结构来实现名称空间
- 名称空间如何在 Python 程序中定义范围
许多编程技术利用了 Python 中每个函数都有自己的名称空间这一事实。在本系列接下来的两篇教程中,您将探索其中的两种技术:函数式编程和递归。
« Regular Expressions: Regexes in Python (Part 2)Namespaces and Scope in PythonFunctional Programming in Python: When and How to Use It »
立即观看**本教程有真实 Python 团队创建的相关视频课程。与书面教程一起观看,加深您的理解: 在 Python 中导航名称空间和范围******


