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
之外,本地命名空间还包括函数参数x
和y
,因为它们也是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行上的赋值语句之后,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'}
16 foo
在本例中,loc
指向local()
的返回值,它是本地命名空间的一个副本。第6行上的语句x = 20
将x
添加到本地名称空间,但不添加到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
语句,声明x
、y
和z
引用全局作用域内的对象。
全局声明中指定的名称不能出现在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提供了关键字global
和nonlocal
,但这些关键字的使用并不总是可取的。
当函数在本地作用域之外修改数据时,无论是使用关键字global
或nonlocal
,还是直接就地修改可变类型,都会产生副作用。这种副作用类似于在函数中修改它的一个参数。一般认为修改全局变量是不明智的,不仅在Python中如此,在其他编程语言中也是如此。
和许多事情一样,这个问题可以归结为风格和偏好。对全局变量进行审慎和明智的修改有时可以降低程序的复杂性。
在Python中,使用关键字global
至少可以明确表示函数正在修改一个全局变量。在许多语言中,函数只需赋值就可以修改全局变量,而不必以任何方式声明它。这使我们非常难以追踪全局数据修改的位置。
总之,在本地作用域之外修改变量通常是不必要的。人们几乎总是有更好的方法,通常使用的是函数返回值。
关注微信公众号:老齐教室
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PerfDog4.0探索,支持用户自建web云
- 跨帐号访问COS资源
- 聊聊dubbo-go的DubboProtocol
- 推荐一个很牛叉的开源Flask项目
- 机器学习应用资产管理系列一:强化学习策略(附代码)
- 3分钟短文 | Laravel模型关联删除表记录,用观察者还是事件钩子
- 原理+代码|深入浅出Python随机森林预测实战
- 3分钟短文 | Laravel复杂SQL超多WHERE子句,本地作用域你没用过
- 3分钟短文 | Laravel同时连接多个数据库,你用啥办法?
- 3分钟短文 | PHP 连接2个字符串的8个方法,新手常犯错
- nodejs源码分析之connect
- 你应该了解的Nacos配置中心
- Jenkins CLI 命令行 v0.0.30
- 2020新鲜出炉的“面筋”,够刁钻
- Spring注解配置应该怎么玩