作用域
什么是作用域
作用域指的是变量生效的区域
python在命名空间(一个保存变量名的地方)中创建、改变、查找变量名
- 变量名以及作用域都是在赋值时创建生成的
- 在代码中变量名被赋值的位置决定了这个变量名将存在哪个空间,也就是能够被访问到的范围。
分类
分类一
在Python中一共有两种作用域
- 全局作用域
- 全局作用域在程序执行时创建,在程序执行结束时销毁
- 所有函数以外的区域都是全局作用域
- 在全局作用域中定义的变量,都属于全局变量,全局变量可以在程序的任意位置被访问
- 每个模块都是一个全局作用域:
- 这里的“全局”作用范围仅限于单个文件,这里的“全局”可以理解为“模块”
-
- 如果想要外部文件中访问,必须先导入模块
- 函数作用域
- 函数作用域在函数调用时创建,在调用结束时销毁
- 函数每调用一次就会产生一个新的函数作用域(本地作用域)
- 在函数作用域中定义的变量,都是局部变量,它只能在函数内部被访问
分类二
Python有四类作用域(Scope)。
- 局部(Local)作用域)
- 封闭(Enclosing)作用域
- 全局(Global)作用域
- 内置(Built-in)作用域
def foo():
b = 'hello'
"""
foo中的变量b,是一个定义在函数中的**局部变量**
属于局部作用域,在foo函数的外部并不能访问到它;
但对于foo函数内部的bar函数来说,变量b属于嵌套作用域,在bar函数中我们是可以访问到它的。
"""
def bar(): # Python中可以在函数内部再定义函数
c = True
print(a)
print(b)
print(c)
"""
bar函数中的变量c属于**局部作用域**,在bar函数之外是无法访问的。
"""
bar()
# print(c) # NameError: name 'c' is not defined
if __name__ == '__main__':
a = 100
"""
if分支中的变量a,是一个全局变量,属于全局作用域,
因为它没有定义在任意一个函数中
"""
# print(b) # NameError: name 'b' is not defined
foo()
变量的查找(LEGB规则)
- 赋值的变量名除非声明为全局变量或者非本地变量,否则均为本地变量
- LEGB规则: 事实上,python查找一个变量时会按照“局部作用域”、“嵌套作用域”、“全局作用域”、“内置作用域”的顺序搜索。所谓的“内置作用域”就是Python内置的那些隐含标识符min、len等都属于内置作用域
- 当我们使用变量时,会优先在当前作用域中寻找该变量,如果有则使用,
- 如果没有则继续去上一级作用域中寻找,如果有则使用,
- 如果依然没有则继续去上一级作用域中寻找,以此类推
- 直到找到全局作用域,依然没有找到,则会抛出异常NameError: name ‘a’ is not defined
- 在作用域中按名称去寻找对象(Python中一切皆对象)时,会按照LEGB规则去查找。 如果发生重名,也会按照LEGB规则,谁先被找到就用谁。
Python没有块级作用域
Python里是没有块级作用域(Block scope)的。
for i in range(10):
pass
print(i)
以上代码会打印出9。
这里是一个比较糟糕的设计。 如果有块级作用域,那么在for块结束后,就不应该能访问i。 而现在,会得到i的最后一个值。
更糟糕的是,如果range()里是0,那么for循环会直接退出。 这时再print(i),结果就是:
NameError: name ‘i’ is not defined
所以,虽然Python没有块级作用域,但是建议就当它有。 不要在代码块以外,使用代码块内定义的东西。
关键字
关键字global
基本用法
(1)
在默认情况下,所有在一个函数中被赋值的对象,是这个函数的本地变量,并且仅在这个函数运行的过程中存在
i = 0
def a():
i = 1
print('local:', i)
a()
print('global:', i)
以上代码的输出结果为:
local: 1
global: 0
也就是说,a函数里的i = 1,是定义一个新的局部变量,而非对全局变量的修改。
如果需要对全局变量i进行修改,则需要使用Python的global关键字。
i = 0
def a():
global i
i = 1
print('local:', i)
a()
print('global:', i)
以上代码的输出结果为:
local: 1
global: 1
global 语句告诉编译器,‘‘在这个函数里,当我说 i 时,我指的是那个全局变量,别生成局部变量’’。如果全局作用域中没有i ,那么下面一行的代码就会定义变量a并将其置于全局作用域。
(2)
如果全局变量是可变的,你可以不加声明地修改它:
known = {0:0 , 1:1}
def example4 () :
known [2] = 1
因此你可以增加、删除和替代全局列表或者字典的元素,但是如果你想对变量重新赋值,你必须声明它:
def example5 () :
global known
known = dict ()
理论
- 全局变量是位于模块文件内部顶层的变量名
- 全局变量在函数的外部创建,属于被称作 __main__ 的特殊帧。 __main__ 中的变量可以被任何函数访问
- 与函数结束时就会消失的局部变量不同,不同函数调用时全局变量一直都存在。
- 全局变量普遍用作标记 (flag);就是说明 (标记) 一个条件是否为真的布尔变量。
使用建议
在实际开发中,我们应该尽量减少对全局变量的使用:
- 因为全局变量的作用域和影响过于广泛,可能会发生意料之外的修改和使用
- 除此之外全局变量比局部变量拥有更长的生命周期,可能导致对象占用的内存长时间无法被垃圾回收。
- 事实上,减少对全局变量的使用,也是降低代码之间耦合度的一个重要举措,同时也是对迪米特法则的践行。
- 减少全局变量的使用就意味着我们应该尽量让变量的作用域在函数的内部,但是如果我们希望将一个局部变量的生命周期延长,使其在函数调用结束后依然可以访问,这时候就需要使用闭包
在文件间进行通信最好的方法就是通过调用函数,传递参数,然后得到其返回值。尽量不要用全局变量
关键字nonlocal
理解
修改Global变量的问题,可以用global关键字来解决; 如果我们希望函数内部的函数能够修改嵌套作用域中的变量,可以使用nonlocal关键字来指示变量来自于嵌套作用域
i = 0
def a():
i = 1
def b():
nonlocal i
i = 2
b()
print(i)
a()
在多重嵌套中,nonlocal只会上溯一层; 而如果上一层没有,则会继续上溯。
def a():
i = 1
def b():
# i = 2
def c():
nonlocal i
i = 3
c()
print('b:', i)
b()
print('a:', i)
a()
以上代码的输出结果为:
b: 3
a: 3
因为函数b中没有i,所以nonlocal上溯到了函数a中。
(注意:如果a中也没有,就回到了Global作用域,nonlocal会报错。)
如果以上代码中,函数b里的注释# i = 2去掉,则输出变为:
b: 3
a: 1
因为nonlocal只会上溯一层,到函数b,所以函数a中的i没有被修改。
注意
-
nonlocal 语句会使得所列出的名称指向之前在最近的包含作用域中绑定的除全局变量以外的变量。 这种功能很重要,因为绑定的默认行为是先搜索局部命名空间。 这个语句允许被封装的代码重新绑定局部作用域以外且非全局(模块)作用域当中的变量。
-
与 global 语句中列出的名称不同,nonlocal 语句中列出的名称必须指向之前存在于包含作用域之中的绑定(在这个应当用来创建新绑定的作用域不能被无歧义地确定)。
-
nonlocal 语句中列出的名称不得与之前存在于局部作用域中的绑定相冲突。
Python 2的问题
LEGB规则是在Python 2.2以后确立的,在那以前不成立,是另一套。
Python 2中没有nonlocal关键字,所以无法对Enclosing作用域的上一层,进行写操作。
小结
- 任何一层子函数,若直接使用全局变量且不对其改变的话,则共享全局变量的值;
- 一旦子函数中改变该同名变量,则其降为该子函数所属的局部变量;
- global 可以用于任何地方,声明变量为全局变量(声明时,不能同时赋值);声明后再修改,则修改了全局变量的值;
- 而 nonlocal 的作用范围仅对于所在子函数的上一层函数中拥有的局部变量,必须在上层函数中已经定义过,且非全局变量,否则报错。
命名空间(namespace)
- 命名空间指的是变量存储的位置,每一个变量都需要存储到指定的命名空间当中
- 每一个作用域都会有一个它对应的命名空间
- 全局命名空间,用来保存全局变量。函数命名空间用来保存函数中的变量
- 命名空间实际上就是一个字典,是一个专门用来存储变量的字典
locals()用来获取当前作用域的命名空间
- 如果在全局作用域中调用locals()则获取全局命名空间,如果在函数作用域中调用locals()则获取函数命名空间
- 返回的是一个字典
scope = locals() # 当前命名空间
print(type(scope))
# print(a)
# print(scope['a'])
# 向scope中添加一个key-value
scope['c'] = 1000 # 向字典中添加key-value就相当于在全局中创建了一个变量(一般不建议这么做)
# print(c)
def fn4():
a = 10
# scope = locals() # 在函数内部调用locals()会获取到函数的命名空间
# scope['b'] = 20 # 可以通过scope来操作函数的命名空间,但是也是不建议这么做
# globals() 函数可以用来在任意位置获取全局命名空间
global_scope = globals()
# print(global_scope['a'])
global_scope['a'] = 30
# print(scope)
fn4()