Python探讨–变量-作用域
1. 作用域
对于有编程基础的伙伴们来说,作用域
是很熟悉的名字了,这里我们仅作简单解释。
大家都知道,在Python中一切皆对象,那么一个对象就好比一个人:在历史长河中,人有生老病死,在软件运行周期中,一个变量有创建,使用,回收(删除);在人类社会中,人有不同的影响范围(智定天下的张良和隔壁村里掏鸟窝的张良不是同一个),在软件程序中,变量有不同的作用域
(全局的变量obj
和局部的obj
不是同一个)
obj = "全局变量"
def function():
obj = "局部变量"
print("obj 是:%s" % obj)
function()
print("obj 是:%s" % obj)
obj 是:局部变量
obj 是:全局变量
作用域:程序代码中,一个变量不是任何地方有效的,而该变量的有效空间范围就是该变量的作用域。简言之,作用域指该变量可被访问(查找)的最大区域。
如下所示,函数内定义的变量obj_l
仅在函数内有效,其作用域为函数function
包含的区域。
obj_g = "全局变量"
def function():
obj_l = "局部变量"
function()
print("全局是否存在变量 obj_g: %s" % ("obj_g" in locals()))
print("全局是否存在变量 obj_l: %s" % ("obj_l" in locals()))
全局是否存在变量 obj_g: True
全局是否存在变量 obj_l: False
- locals :内置方法,返回值是字典格式,返回当前命名空间内的变量
2. 命名空间
2.1 定义
什么是命名空间?或者说,当程序运行时我们需要访问变量(假设变量名为 obj ),那么解释器如何找到该变量呢?
程序运行时,对于每一个函数、类、模块(乃至于全局的builtins)都会开辟一块用于存储当前区域内声明的变量-值
的内存空间,这就是命名空间。
简言之,每个函数、类都对应一个命名空间,该空间内存放着一张表,表里记录了函数、类中定义的变量及其内容。
def function():
obj_l = "局部变量"
print("局部命名空间: %s" % locals())
function()
print("模块命名空间: %s" % locals())
局部命名空间: {'obj_l': '局部变量'}
模块命名空间: {
'__file__': '/home/admin/python/test.py',
'__builtins__': <module 'builtins' (built-in)>,
'__name__': '__main__',
'obj_g': '全局变量'}
...
}
- python中命名空间采用字典格式存储数据。
- 解释器在命名空间内查找变量。
- python是解释性语言,从上往下执行时不断向命名空间内添加内容。
2.2 划分
2.2.1 Local
局部命名空间(local)指在一个函数或一个类中定义的变量或引用的字典集合,使用locals()即可获得局部的命名空间,返回一个字典。
2.2.2 Enclosed
闭包中的命名空间,用于存储外层的局部变量。
def function():
obj_l = "enclosed 变量"
def wrapper():
obj_l = "local 变量"
print("local命名空间: %s" % locals())
print("enclosed命名空间: %s" % locals())
wrapper()
function()
enclosed命名空间: {'obj_l': 'enclosed 变量', 'wrapper': <function function.<locals>.wrapper at 0x7fb0b6462ea0>}
local命名空间: {'obj_l': 'local 变量'}
- 可以看出,wrapper方法执行时,解释器优先使用局部命名空间内的变量。这个顺序后面会讲到。
2.2.3 Global
模块命名空间(Global)即在当前模块中的所有的对象的字典集合,使用globals()获取模块命名空间。
from datetime import datetime
def function():
obj_l = "局部变量"
print("局部命名空间: %s" % locals())
function()
print("模块命名空间: %s" % locals()) #等价于globals
局部命名空间: {'obj_l': '局部变量'}
模块命名空间: {
'__file__': '/home/admin/python/test.py',
'__builtins__': <module 'builtins' (built-in)>,
'__name__': '__main__',
'__package__': None,
'obj_g': '全局变量'},
'datetime': <class 'datetime.datetime'>,
...
}
- 导入的模块内容也会添加进去
- 在全局范围下使用locals方法与globals方法效果相同,即局部命名空间的“局部”是相对的,指当前语句所处位置的命名空间。
2.2.4 builtin
全局命名空间(builtin),是python的全局命名空间(囊括了当前Python环境中所有导入的内容)。
思考一个问题,为什么我们没有定义dict、list等类型,locals、globals等方法,却可以使用它。结合之前所学,解释器一定在某个命名空间内能够找到他,但之前三个命名空间内都没有找到这部分内容。
全局命名空间(builtin):是python全局的命名空间(囊括了当前Python环境中所有导入的内容),所有的内置类型、方法、变量都在里面进行存储。
之前使用globals
方法查看时,返回了一个内容'__builtins__': <module 'builtins' (built-in)>
。每个python模块都会有这样一个变量,用于指向自己的全局命名空间。
2.3 LREGB
2.3.1 简单分析
之前我们了解到,每个层次(方法,类,全局,模块等)都对应一个命名空间,每个命名空间(形如沙盒)相互独立,那么解释器在寻找变量时,要如何依次访问这些命名空间呢?
obj_g = "全局变量"
def function():
obj_l = "enclosed 变量"
def wrapper():
obj_l = "local 变量"
print("内层函数访问obj_l: %s" % obj_l)
wrapper()
print("外层函数访问obj_g: %s" % obj_g)
function()
内层函数访问obj_l: local 变量
外层函数访问obj_g: 全局变量
下面简单分析
- 内层函数访问obj_l:wrapper局部命名空间和闭包的enclosed命名空间内均有变量obj_l,解释器采用了局部的。
- 外层函数访问obj_g:enclosed命名空间内没有变量obj_g,解释器采用了全局的。
2.3.2 洋葱模型
简单来说,解释器查询变量时对命名空间的访问如下:
解释器从当前命名空间开始找,如果找到就使用(上例中local的覆盖了enclosed的),解释器如果没找到,则从内而外依次寻找,若builtins都未找到,抛出异常(NameError)。这便是python的LEGB规则,我喜欢用洋葱模型取描述,类似的情形以后会大量碰到。
3. 跨空间使用
3.1 简介
由pythonLEGB
规则得知,我们可以访问上层命名空间内的变量,那么是否可以修改呢?
obj = "全局变量"
mutable = [1, 2]
def function():
mutable[0] += 1 #通过
obj += "!" #异常
function()
- 对于不可变类型,python不允许低层修改,只允许访问;
- 对于可变类型,python允许低层修改,允许访问
3.2 跨域修改
那么,对于不可变类型真的无法修改了吗?
obj = "全局变量"
mutable = ["全局变量"]
def function():
en_obj = "外层变量"
def wrapper():
mutable[0] += "!"
nonlocal en_obj
en_obj += "!"
global obj
obj += "!"
wrapper()
print(en_obj)
function()
print(obj)
print(mutable)
外层变量!
全局变量!
['全局变量!']
3.2.1 化不变为可变
参考mutable,将不可变(字符串)转化为可变的列表(封装)。底层就可以去修改内容,即python对底层的限制修改仅局限于变量本身是否为不可修改类型。
3.2.2 关键字
- global
变量名前加global
,表示声明该变量在全局命名空间内。解释器会直接去全局中访问,相当于声明了一个全局变量。
- nonlocal
变量名前加nonlocal
,表示声明该变量在上一层命名空间内。解释器会去上层命名空间中访问,注意的是,上一层不得是全局,通常时闭包中使用.
4. 笔录
- 不要在局部中修改上层变量,容易出事。推荐使用将结果return的方式,在调用端接受即可。
def function(obj):
obj += "!"
return obj
obj = "test"
obj1 = function(obj)
同样得到结果,同时避免出错
- for循环
python的for循环同c++, java的for循环不同,python的for循环更像js的foreach循环,虽然速度更快,但导致更多陷阱的出现。其中包括,对外部空间的污染。
for i in range(4):
if i == 1:
print(locals())
break
print(locals())
{
'__name__': '__main__',
'__builtins__': <module 'builtins' (built-in)>,
'i': 1
...
}
{
'__name__': '__main__',
'__builtins__': <module 'builtins' (built-in)>,
'i': 1
}