作用域是函数调用时非常重要的一个概念,那作用域到底是什么呢?
顾名思义 作用域 其实就是起作用的范围
我们先从变量说起.
变量到底是什么呢?可已把它看做指向值的名称.我们在使用变量时,其实和使用字典差不多.
x = 1
变量到值的关系几乎跟字典中的键到值的关系一致,其实,当你声明变量时,Python确实生成了一个你"看不见"的字典
x = 1
scope = vars()
print(scope)
# {'__builtins__': , '__file__': '/Users/shihangwei/Documents/code/01.py', '__package__': None, 'scope': {...}, 'x': 1, '__name__': '__main__', '__doc__': None}
我们通过vars()
函数可以显式的看到一个字典,x:1
很明显就是我们的变量声明。
x
起作用的范围,就是在这个 "看不见"的字典中,我们把这些字典称为命名空间或者作用域
一般而言 Python 不建议通过 vars 的修改来改变变量的值。这会导致一些"不确定"的后果
那么这些"看不见"的字典到底有多少个呢?既然作用域是其作用的范围,那么范围到底怎么划分呢?
先写一个简单的例子
if 1 == 1:
name = "change"
print(name)
这里我们需要思考变量name
起作用的范围了,在条件判断语句代码块以外,name
生效吗?
答案是肯定的
"change"
我们同样可以在代码块外打印vars()
的返回结果
if 1 == 1:
name = "change"
print(vars())
# {'name': 'change', '__builtins__': , '__file__': '/Users/shihangwei/Documents/code/01.py', '__package__': None, '__name__': '__main__', '__doc__': None}
在代码块外部,我们可以看到作用域内存在name
变量以及其值
这就表明在Python中,代码块里的变量,代码块外可以访问到,Python中不存在块级作用域
在Java/C#中,执行上面的代码会提示name没有定义,这些语言是存在块级作用域的
在代码块之后,我们探究一下函数的声明是否影响了变量起作用的范围,下列代码为例
def fun():
name = "lisi"
print(name)
我们能够在函数外部打印出name
的值吗?答案是否定的
Traceback (most recent call last):
File "/Users/shihangwei/Documents/code/01.py", line 4, in
print(name)
NameError: name 'name' is not defined
有的同学可能认为,函数没有运行,name
的赋值动作没有执行,函数外部没有打印出值是很正常的,那我们执行一下fun
def fun():
name = "lisi"
print(vars()) # 1
fun()
print(vars()) # 2
print(name) # 3
运行结果如下
{'name': 'lisi'} # 1
{'__builtins__': , '__file__': '/Users/shihangwei/Documents/code/01.py', '__package__': None, 'fun': , '__name__': '__main__', '__doc__': None} # 2
Traceback (most recent call last): # 3
File "/Users/shihangwei/Documents/code/01.py", line 7, in
print(name)
NameError: name 'name' is not defined
我用# 1
,# 2
,# 3
标注了每一步的执行结果,我们可以清晰的看到
# 1
是函数执行时打印的结果,在该字典中存在name
的值,变量name
在函数范围内生效# 2
是在函数外部打印的结果,在该字典中存在函数体fun
,但并没有变量name
,该变量在函数体外不生效,所以# 3
才会执行出现错误函数内部的变量在外部无法直接访问,也无法直接影响外部变量。这也是为什么函数内部的变量可以和外部同名的原因,以下的结果也就不言而喻了
def fun():
name = "lisi"
fun()
name = "Lily"
print(name)
# "Lily"
我们可以依此将作用域划分为以下两种
局部作用域/命名空间:函数声明时创造的作用域,变量只能在被声明的函数内部访问,函数外部使用不了。
全局作用域/命名空间:最外层的作用域
而变量也可以由上述两个作用域被分为局部变量和全局变量
除了使用vars
函数以外,我们还可以使用globals
和locals
函数打印当前位置的全局和局部作用域
name = "Lily"
def fun():
name = "lisi"
print(locals())
print(globals())
fun()
print(name)
# {'name': 'lisi'}
# {'name': 'Lily', '__builtins__': , '__file__': '/Users/shihangwei/Documents/code/01.py', '__package__': None, 'fun': , '__name__': '__main__', '__doc__': None}
# Lily
也可以清晰的看出局部变量和全局变量的name
不同
请注意,locals() 打印的是当前位置的局部作用域,如果我在最外层使用 locals,它得到的结果和 global 应该是一样的
在上文中,我们得出了一个结论,全局作用域不能访问局部作用域中的局部变量,那么反过来,局部作用域能不能访问全局作用域中的全局变量呢?
name = "lisi"
def fun():
print(name)
fun()
答案竟然是肯定的!
"lisi"
我们再来看一看各自作用域中的变量
name = "lisi"
def fun():
print(locals())
print(globals())
print(name)
fun()
# {}
# {'name': 'lisi', '__builtins__': , '__file__': '/Users/shihangwei/Documents/code/01.py', '__package__': None, 'fun': , '__name__': '__main__', '__doc__': None}
# lisi
不出意料,局部作用域是空的,但是我们的name
变量看上去像是跳出了函数的作用域,到全局中寻找了。
其实我们简单思考一下就能明白其中的原因。
单个Python文件可以看作一个全局的作用域,但是有没有比单个Python文件更为广阔的变量存储的空间呢?当然有,我们的内置函数,内置变量,其实在Python解释器刚跑起来的时候已经被创建了。
我们已经习惯了在Python文件中调用没有自定义的内置函数,这其实是一个更大范畴的向上寻找。局部和全局也都是相对应的,而局部作用域可以访问全局作用域中的变量。
局部访问全局时,我们仍然需要注意一些问题
如果局部变量和全局变量同名,则会发生"覆盖”
道理很简单,你在局部已经有了,干嘛还要跳出来向全局要啊?
name = "lisi"
def fun():
name = "lily"
print(name)
fun()
# lily
在函数内部不要尝试修改全局变量
虽然我们可以通过某些方法达到这个目的,但是尽量还是不要在局部作用域中修改全局变量的值,这样会给项目带来莫名其妙的bug
name = "lisi"
def fun():
name = "Hello" + name
print(name)
fun()
# UnboundLocalError: local variable 'name' referenced before assignment
局部作用域可以访问全局作用域中的变量,这里我们先得到一个结论,接下来我们将更深入的介绍作用域的划分。
在上文中我们了解到全局作用域和局部作用域和他们的访问规则,也了解到了在全局作用域之外,系统也给我们准备了一系列的内置变量/函数,那么在Python当中,作用域到底有哪些呢?
L(Local)局部作用域
局部变量:包含在def关键字定义的语句块中,即在函数中定义的变量。每当函数被调用时都会创建一个新的局部作用域。在函数内部的变量声明,除非特别的声明为全局变量,否则均默认为局部变量。局部变量域就像一个 栈,仅仅是暂时的存在,依赖创建该局部作用域的函数是否处于活动的状态。所以,一般建议尽量少定义全局变量,因为全局变量在模块文件运行的过程中会一直存在,占用内存空间。
E(enclosing)嵌套作用域
E也包含在def关键字中,E和L是相对的,E相对于更上层的函数而言也是L。与L的区别在于,对一个函数而言,L是定义在此函数内部的局部作用域,而E是定义在此函数的上一层父级函数的局部作用域。
G(global)全局作用域
即在模块层次中定义的变量,每一个模块都是一个全局作用域。也就是说,在模块文件顶层声明的变量具有全局作用域,从外部开来,模块的全局变量就是一个模块对象的属性。
注意:全局作用域的作用范围仅限于单个模块文件内
B(built-in)内置作用域
系统内固定模块里定义的变量,即系统自带的的变量函数之类的。
我们用以下代码和图片表明LEGB的作用范围
name = "lisi" #G
def get_name():
name = "wangwu" #E
def get_inner_name:
name = "lucy" #L
print(__name__) #B
LEGB 作用范围依次增大。很明显,我们在上文的到的结论"局部作用域可以访问全局作用域中的变量"就可以做一些更改了。同样,我们做一下实验
name = "lisi"
def get_name():
def get_outer_name():
print(name)
get_outer_name()
get_name()
# "lisi"
print(vars)
#
答案显而易见,Python 中寻找变量时,如果在局部作用域中有,就直接使用,如果没有,就去上级作用域去寻找,直到全局作用域,全局作用域还没有,再去内置作用域寻找,再没有,就会报错。
但是反过来,无论如何,上级作用域是无法访问到下级作用域的。
def get_name():
def get_outer_name():
name = "lisi"
get_outer_name()
print(name)
get_name()
# NameError: global name 'name' is not defined
一句话总结,在寻找变量时,查找的顺序是LEGB,并且不能反过来寻找。
name = "lisi"
def f1():
name = "wangwu"
def f2():
name = "lucy"
print(name)
f2()
f1()
Python中有作用域链,变量会由内到外找,先去自己作用域去找,自己没有再去上级去找,直到找不到报错,根据执行顺序,函数打印时的name
就是离他最近的那个,所以结果为lucy
name = "lisi"
def f1():
print(name)
def f2():
name = "wangwu"
f1()
f2()
这段代码看似很简单,但是也需要一定的思考。f2
执行当中有f1
的调用,那么f1
在寻找name
这个变量的时候,到底是从f2
作用域中寻找还是在全局中寻找呢?
这里需要明确一点,函数的作用域在声明时就已经决定了,和他在声明地方调用,没有任何关系。
函数的作用域取决于声明时,而不取决于调用时
所以,答案显而易见,最终的结果是lisi
上文中我们已了解到 Python 中变量的查找规则。接下来我们探讨这样一个问题,如何通过函数修改全局变量呢?
num = 10
def change_num():
num = 20
change_num()
print(num)
这种做法我们可以直接否定了。刚才我们已经知晓了,局部作用域中的变量声明只是声明了局部变量,和全局变量没有什么瓜葛。所以无论函数内声明的变量值为多少,都不是对全局变量的复制,只是对自身的赋值。
那么这种做法呢?
num = 10
def change_num():
num = num + 10
change_num()
print(num)
如果这样书写,会直接报错,
UnboundLocalError: local variable 'num' referenced before assignment
这也很好理解,我们在函数内不想声明num
,而是希望拿到全局中的num
运算后重新赋值。
但在运算操作实质上是想要改变num
的引用,num
由10变为20,不只是值发生了变化,num
所指向的地址也会发生变化,对于上级/全局变量的修改不能如此随意的在局部作用域完成,否则我们的代码会出现很多bug,因为函数可以随意的污染全局变量。
>>> num = 10
>>> id(num)
4447551920
>>> num = num + 10
>>> id(num)
4447552240
可以看到,num的地址被改变了。
如果我们想要在局部作用域修改全局变量,可以使用global
关键词在变量声明时进行修饰
num = 18
def change_num():
global num
print(num) # 18
num += 20
change_num()
print(num) #38
这样我们就可以在局部作用域访问并修改全局变量了。
并且global
关键字可以应用到任何位置
num = 10
print(id(num)) # 140195471071504
def change_outer():
num = 20
def change_inner():
global num
print(num) # 10
print(id(num)) # 140195471071504
change_inner()
change_outer()
虽然有函数嵌套,但是用global
修饰的变量永远和全局同名变量保持一致
除了局部作用域和全局作用域出现上述场景,局部作用域和嵌套作用域同样会出现,因为都是由小范围的作用域,访问较大范围的作用域
def change_outer():
num = 10
def change_inner():
num = num + 10
change_inner()
change_outer()
# UnboundLocalError: local variable 'num' referenced before assignment
出现了同样的错误,我们做一下类比,很容易就能想到,change_outer
函数作用范围比较大,相当于"全局",而change_inner
函数嵌套在内部,可以看作"局部",同理,上述代码运行也会报错,因为同样是内部作用域尝试改变外部作用域中变量的引用,这是绝对不允许的。
但是此时我们不能过用global
关键字,因为global
关键字无论在何处声明变量,关联的均为全局变量。现在我们想要在嵌套作用域中访问并修改上级作用域中的变量,可以使用nolocal
关键字
def change_outer():
num = 10
def change_inner():
nonlocal num
num = num + 10
change_inner()
print(num) # 20
change_outer()
这样,通过nonlocal
修饰,在嵌套作用域可以访问并修改上层的局部作用域中的变量。我们也能理解,范围的大小是相对而言的。
同样,两层的变量num
如果不做更改,实质上是同一个引用
def change_outer():
num = 10
print(id(num)) # 4562293168
def change_inner():
nonlocal num
print(id(num)) # 4562293168
change_inner()
change_outer()
同时需要注意,nonlocal
既然是修饰嵌套作用域内的变量,那就不能够出现在全局,也不能出现在最外层函数内部。以下的写法都是错误的
num = 10
def change_outer():
nonlocal num
num = 20
change_outer()
# SyntaxError: no binding for nonlocal 'num' found
nonlocal num
def change_outer():
num = 20
change_outer()
# SyntaxError: nonlocal declaration not allowed at module level
闭包是一个很有趣的语法现象。在讲解闭包之前,我们首先需要了解一下Python中函数的几个有意思的特殊用法。
>>> def func():
... print("hello")
...
>>> func
>>> func2 = func
>>> func2
>>> func2()
hello
我们可以把函数名看作函数体的一个地址的引用。
赋值操作其实是在传递函数体的地址的值。
def func():
print(123)
def func2(f):
f()
func2(func)
name = "wangwu"
def func():
name = 'lisi'
def func1():
print(name)
return func1
func2 = func()
func2()
我们知道,函数名记录的是函数体在内存中的地址,无论以什么方式传递,只是对地址的传递。
那么问题来了,上面的代码,结果是lisi
还是wangwu
?
如果我们把func
的返回值看作一个独立的函数,和上下文没有关系,那么结论可能是输出wangwu
,因为我在全局执行的嘛。但是实际上并不是这样。
我们打印一下地址
name = "wangwu"
def func():
name = 'lisi'
def func1():
print(name)
print(id(func1)) # 4504575784
return func1
func2 = func()
print(id(func2)) # 4504575784
func2()
外部返回的函数仍然是我们定义的局部函数!
在大部分语言当中,func
被调用执行,则申请内存,执行完毕,内部的局部变量随着函数的退出而销毁,name='lisi'
的依赖已经消失。
但是在Python当中name='lisi'
这个变量却仍然能被func1
捕捉。即使func
执行完毕,通过func1
仍然能访问该变量。
这种情况:当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体返回。我们称之为闭包。
闭包其实就是由函数及其相关的引用环境所组成的实体,所谓的引用环境,其实就是函数在调用时的那一瞬间,和它相关的各种约束(包括变量和变量指向的值)组成的一个集合。
简单来说在上面的例子中,func1
就是外部调用的函数,而函数内部没有变量name
,需要从它定义时所处的环境中寻找,那么name="lisi"
其实就可以看作引用环境。
我们来看一个例子
def func(n):
sum = n
def func1():
return sum+1
return func1
myfunc = func(10)
print(myfunc())
# 11
myfunc2 = func(20)
print(myfunc2())
# 21
当调用分别由不同参数调用func
得到的函数时,myfunc
和myfunc1
得到的结果是隔离的。也就是每次调用func
后都生成并保存一个新的局部变量sum
。其实此处func
返回的就已经不是函数本身了。而是一个闭包。
由于闭包把函数和运行时的环境打包成为一个整体,它已经不同于函数了。闭包在运行时可以有多个实例,不同环境的引用和函数的组合可以产生多个实例。
上文我们讲了一些概念,现在我们讲几个闭包的应用
先看下面的例子
flist = []
for i in range(3):
def foo(x): print(x + i)
flist.append(foo)
for f in flist:
f(2)
这是一个经典闭包应用。通过阅读代码同学们可能觉得结果是2,3,4
,但是结果却是4,4,4
。
这是为什么呢?容易忽略的点就是,在for循环内定义函数,没有产生作用域嵌套,因为for
语句并没有能力产生块级作用域。
所以当我们循环调用flist
中的函数时,i
的值一直是第一个循环的最终值,2
!
如何解决呢?很简单,我们只需要在第一个循环时,将i
装进函数就行了
flist = []
for i in range(3):
def foo(x,y=i): print(x + )
flist.append(foo)
for f in flist:
f(2)
在实际开发过程当中,闭包一般用于保持函数的运行环境。如果你希望函数的每次执行结果都是基于上次的运行结果实现的,就可以使用闭包。
假设我们要写一个下棋的游戏。很显然,如果想做下棋的动作,棋子的移动必定是相对于上次位置。并且每次移动的最终位置又要作为下次移动的初始值。这时候闭包的优势就体现出来了
origin = [0,0] # 棋盘原点
def create(pos = origin):
def move(direct,step): # direct 是运动方向 [1,0]表示x轴正方向 step是运动长度
new_x = pos[0] + direct[0]*step
new_y = pos[1] + direct[1]*step
pos[0] = new_x
pos[1] = new_y #下次出发的位置,保存在pos中,留在闭包内
return pos
return move
move = create()
print(move([1,0],10)) # [10] x轴移动10步之后的位置
print(move([0,1],20)) # [] 在上一步的基础上,y轴正方向移动20步
Python 不存在块级作用域,我们可以使用def、class、lambda等语句产生局部作用域
作用域查找的顺序是LEGB,由小到大,反之不行。
函数的作用域取决于声明时,不取决于调用时
作用域的大小是相对的,内外也是相对的。通过global
可以获得全局变量的引用,通过nonlocal
获得上层作用域变量的引用
nonlocal
不可用于全局或者最外层函数内部。
闭包其实就是由函数及其相关的引用环境所组成的实体,在访问函数时,函数定义时所处的环境仍然起作用