编译:老齐
相关图书推荐:《Python大学实用教程》
这是一本针对零基础的初学者学习Python的书,强调开发实战,在学习中体会了解真实的开发需要。
本文将介绍Python命名空间和作用域,它们用于分配Python程序中的对象。Python语言是一种能够实现面向对象编程的高级语言,或者说,在Python中,“万物皆对象”。
例如,x = 'foo'
中的x
是一个变量,它应用了字符串对象'foo'
。
在一个复杂的程序中,会创建成百上千个这样的变量名称或者函数名称、类名称等,每个名称都指向特定的对象。Python如何跟踪所有这些名称,以便它们不会相互干扰呢?
接下来就解决这个问题。
命名空间
命名空间是当前定义的符号名称以及每个符号名称所引用的对象的信息的集合。可以将命名空间视为字典,其中键是对象名称,值是对象本身。每个键值对将一个名称映射到它所对应的对象。
正如《Python之禅》中所说的那样:命名空间是一个很棒的创意,让我们多做些这样的创意!
何止很棒,简直是绝妙。
在Python中,一共有三种类型的命名空间:
- 内置(built-in),Python语言内置的名称,比如函数名
abs
、char
和异常名称BaseException
、Exception
等等。 - 全局(global),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
- 局部(local),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnlj7ooW-1597021997987)(https://imgkr2.cn-bj.ufileos.com/40e2622c-efe1-4b21-aa57-b8781c1cf7da.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=%252FIom7ubgk0hsZNMw7nMx0Lhyb1s%253D&Expires=1597019512)]
每个命名空间有不同的声明周期,当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']
Python解释器在启动时直接创建内置命名空间,并且这个命名空间一直存在,直到解释器终止。
全局命名空间
全局命名空间包含主程序级别定义的任何名称。Python在主程序启动时创建全局命名空间,它一直存在,直到解释器终止。
严格地说,这可能不是唯一存在的全局命名空间。解释器还为程序使用import
语句加载的任何模块创建一个全局命名空间。
局部命名空间
局部命名空间,也可以翻译为“本地命名空间”。比如函数,每一个函数一旦运行,就创建了一个新的命名空间,这个命名空间是函数的本地命名空间,它的存在一直持续到函数终止。
函数并非彼此独立存在的,而且这种关联不限于主程序级别的函数,你也可以在另一个函数中定义一个函数,即嵌套函数:
>>>
>>> def f():
... print('Start f()')
...
... def g():
... print('Start g()')
... print('End g()')
... return
...
... g()
...
... print('End f()')
... return
...
>>> f()
Start f()
Start g()
End g()
End f()
在本例中,函数g()
是在f()
的内定义的,这种方式所定义的函数称为嵌套函数,也称为“闭包”——更详细解释,请参阅《Python大学实用教程》一书的有关章节。
当主程序调用f()
时,Python会为f()
创建一个新的命名空间。类似地,当f()
调用g()
时, g()
将获得自己独立的命名空间。为g()
创建的命名空间是本地命名空间,为f()
创建的命名空间是闭包命名空间——与g()
的命名空间名称区分,也可以认为两个都是局部命名空间。
局部命名空间的声明周期是自其建立开始,到它们各自的函数执行完毕终止。当这些命名空间的函数终止时,Python可能不会立即回收分配给这些命名空间的内存,但是对其中对象的所有引用都将失效。
变量作用域
有多个不同命名空间,这就意味着允许Python程序中可以在不同的命名空间中有几个不同实例同时存在——但是这些实例的名称相同。只要每个实例在不同的命名空间,它们都是单独维护的,不会相互干扰。
但这就产生了一个问题:假设你在代码中引用了名称x
,并且x
存在于多个命名空间中。Python怎么知道你指的是哪个命名空间?
答案就是“作用域”。名称的作用域是某个程序的区域,而在这个区域中该名称具有意义。解释器在运行时根据名称定义的位置以及名称在代码中被引用的位置来确定这一点。
例如代码中引用名称x
,那么Python将按照以下的顺序搜索x
:
- 本地作用域:如果你在一个函数中引用
x
,那么解释器首先在该函数本地的最内部作用域内搜索它。 - 闭包作用域:如果
x
不在本地作用域中,而是出现在另一个函数内部的函数中,则解释器将搜索闭包函数的作用域。 - 全局作用域:如果以上两个搜索都没有结果,那么解释器接下来会查看全局作用域。
- 内置作用域:如果在其他地方找不到
x
,那么解释器将尝试内置的作用域。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tk5tksxH-1597021997988)(https://imgkr2.cn-bj.ufileos.com/91ecb61e-223b-416b-8744-6488f3e27b09.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=N1udfqxlwg5B443WuIJxVbPvKn8%253D&Expires=1597022094)]
这是Python文献中通常所称的LEGB规则(尽管Python文档中并没有实际出现这个术语)。解释器从内到外搜索名称,查找本地、闭包、全局,最后是内置作用域。
如果解释器在这些位置中找不到名称,那么Python将抛出NameError异常。
下面是LEGB规则的几个例子。在每种情况下,最里面的闭包函数g()
都试图向控制台显示名为x
的变量的值。注意每个示例如何根据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()
12 global
第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()
13 enclosing
与上一个示例一样,g()
引用了x
。但这一次,它有两个定义可供选择:
- 第1行定义了全局作用域内的
x
。 - 第4行在闭包作用域内再次定义了
x
。
根据LEGB规则,解释器在查找全局作用域之前,先从闭包作用域中找到值。所以第7行的print()
语句显示“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()
14 local
现在第8行的print()
语句必须区分三种不同的可能性:
- 第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()
10 Traceback (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
14 NameError: name 'x' is not defined
这一次,Python在任何命名空间中都找不到x
,因此第4行的print()
语句抛出NameError
异常。