Python中的命名空间和作用域(2)

时间:2022-07-23
本文章向大家介绍Python中的命名空间和作用域(2),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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'] 
 7 True

第6行的比较证实了这些实际上是同一个对象。

还可以使用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
 10 100
 11 
 12 >>> globals()['y'] = 3.1415913 
 14 >>> y
 15 3.14159

第1行的语句与赋值语句y = 100具有相同的效果。第12行的语句相当于y = 3.14159

当简单的赋值语句就可以奏效时,就不要用globals()来修改了,但它确实有效,而且很好地说明了这个概念。

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()时,locals()返回表示函数的本地命名空间的字典。注意,除了本地定义的变量s之外,本地命名空间还包括函数参数xy,因为它们也是f()的本地参数。

如果在函数外部调用locals() ,那么它与globals()用法相同。

深入探究

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行上的赋值语句之后,xy出现在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'}
16 foo

在本例中,loc指向local()的返回值,它是本地命名空间的一个副本。第6行上的语句x = 20x添加到本地名称空间,但不添加到loc指向的副本。类似地,第9行上的语句修改了loc所指向的副本中的键‘s'的值,但这对实际本地名称空间中的``s的值没有影响。

这是一个微妙的区别,但如果你不记住的话,可能会给你带来麻烦。

修改作用域之外的变量

如果你已经读过《Python大学实用教程》这本书,一定已经知道Python中函数的参数,有的是按位置引用,有的是按值引用;有的参数值能够修改,有的不能修改。

下面代码演示了函数试图在其本地作用域之外修改变量时出现的问题:

 1 >>> x = 20 
 2 >>> def f(): 
 3 ...     x = 40 
 4 ...     print(x) 
 5 ... 
 6  
 7 >>> f() 
 8 40 
 9 >>> x
 10 20

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进行更改,尽管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()试图修改可变函数参数时所发生的情况。

全局声明

如果确实需要从f()中修改全局作用域中的值,该怎么办? 在Python中使用全局声明是可行的:

>>> x = 20
>>> def f():
...     global x
...     x = 40
...     print(x)
...
>>> f()
40
>>> x
40

global x语句表明,当f()运行时,对名称x的引用将指向全局命名空间中的x。这意味着赋值x = 40不会创建一个新的引用。它在全局作用域内给x赋了一个新值:

前面已经介绍过,globals()返回对全局命名空间字典的引用。如果你愿意,可以使用globals() 代替global语句来完成相同的任务:

>>> x = 20
>>> def f():
...     globals()['x'] = 40
...     print(x)
...
>>> f()
40
>>> x
40

完全没有必要这样做,因为全局声明已经较为明确地表达了这种做法的意图。但它确实为globals() 的应用提供了另一个例证。

如果全局声明中指定的名称在函数启动时不存在于全局作用域中,则global语句和赋值的组合将创建这一名称:

 1 >>> y 
 2 Traceback (most recent call last): 
 3   File "<pyshell#79>", line 1, in <module> 
 4     y 
 5 NameError: name 'y' is not defined 
 6  
 7 >>> def g(): 
 8 ...     global y 
 9 ...     y = 20
10 ...
11 
12 >>> g()
13 >>> y
14 20

g()开始运行时,在全局作用域内没有名为y的对象,但是g()在第8行使用global y 语句创建了一个这样的对象。

你也可以在单个全局声明中指定用多个逗号分隔的名称:

 1 >>> x, y, z = 10, 20, 30 
 2  
 3 >>> def f(): 
 4 ...     global x, y, z 
 5 ...

在这里,我们通过第4行的单个global语句,声明xyz引用全局作用域内的对象。

全局声明中指定的名称不能出现在global语句之前的函数中:

 1 >>> def f(): 
 2 ...     print(x) 
 3 ...     global x 
 4 ... 
 5   File "<stdin>", line 3 
 6 SyntaxError: name 'x' is used prior to global declaration

第3行上的global x语句的目的是让对x的引用指向全局作用域中的一个对象。但是第2行的print()语句指向全局声明之前的x,这会引发SyntaxError异常。

非本地声明

嵌套函数的定义也存在类似的情况。全局声明允许函数访问和修改全局作用域中的对象。如果一个闭包函数需要修改闭包作用域的对象该怎么办?考虑一下这个例子:

 1 >>> def f(): 
 2 ...     x = 20 
 3 ... 
 4 ...     def g(): 
 5 ...         x = 40 
 6 ... 
 7 ...     g() 
 8 ...     print(x) 
 9 ...
10 
11 >>> f()
12 20

在本例中,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()
 13 40

在第5行nonlocal x语句之后,当g()引用x时,它指的是最近的闭包作用域内的x,其定义在f()中的第2行。

第9行的print()语句确认对g()的调用已将闭包作用域内的x值更改为40

最佳实践

尽管Python提供了关键字globalnonlocal,但这些关键字的使用并不总是可取的。

当函数在本地作用域之外修改数据时,无论是使用关键字globalnonlocal,还是直接就地修改可变类型,都会产生副作用。这种副作用类似于在函数中修改它的一个参数。一般认为修改全局变量是不明智的,不仅在Python中如此,在其他编程语言中也是如此。

和许多事情一样,这个问题可以归结为风格和偏好。对全局变量进行审慎和明智的修改有时可以降低程序的复杂性。

在Python中,使用关键字global至少可以明确表示函数正在修改一个全局变量。在许多语言中,函数只需赋值就可以修改全局变量,而不必以任何方式声明它。这使我们非常难以追踪全局数据修改的位置。

总之,在本地作用域之外修改变量通常是不必要的。人们几乎总是有更好的方法,通常使用的是函数返回值。

关注微信公众号:老齐教室