做用域是指变量的生效范围,例如本地变量、全局变量描述的就是不一样的生效范围。html
python的变量做用域的规则很是简单,能够说是全部语言中最直观、最容易理解的做用域。python
在开始介绍做用域以前,先抛一个问题:安全
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
上面的代码将输出三、一、1。解释参见再述做用域规则。另外,我的建议,本文最后一小节内容尽可能理解透彻。多线程
它有4个层次的做用域范围:内部嵌套函数、包含内部嵌套函数的函数自身、全局做用域、内置做用域。上面4个做用域的范围排序是按照从内到外,从小到大排序的。闭包
其中:app
__builtins__
模块中。这些名称主要是一些关键字,例如open、range、quit等因此对于下面这段python代码来讲,若是它处于a.py文件中,且没有嵌套在其它函数内:函数
X=1 def out1(i): X=2 Y='a' print(X) print(i) def in1(n): print(n) print(X,Y) in1(3) out1(2)
那么:
处于全局做用域范围的变量有:X、out1
处于out1本地做用域范围的变量有:i、X、Y、in1
处于嵌套在函数out1内部的函数in1的本地做用域范围的变量有:n工具
注意上面的函数名out1和in1也是一种变量。测试
以下图所示:优化
当在某个范围引用某个变量的时候,将从它所在的层次开始搜索变量是否存在,不存在则向外层继续搜索。搜索到了,则当即中止。
例如函数ab()中嵌套了一个函数cd(),cd()中有一个语句print(x)
,它将首先检查cd()函数的本地做用域内是否有x,若是没有则继续检查外部函数ab()的本地做用域范围内是否有x,若是没有则再次向外搜索全局范围内的变量x,若是仍是没有,则继续搜索内置做用域,像"x"这种变量名,在内置做用域范围内是不存在的,因此最终没有搜索到,报错。若是一开始在cd()中就已经找到了变量x,就不会再搜索ab()范围以及更外层的范围。
因此,内层范围能够引用外层范围的变量,外层范围不包括内层范围的变量。
内置做用域主要是一些内置的函数名、内置的异常等关键字。例如open,range,quit等。
两种方式能够搜索内置做用域:一是直接导入builtins模块,二是让python自动搜索。导入builtins模块会让内置做用域内的变量直接置于当前文件的全局范围,自动搜索内置做用域则是最后的阶段进行搜索。
通常来讲无需手动导入builtins模块,不过能够看看这个模块中包含了哪些内置变量。
>>> import builtins >>> dir(builtins) ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', ............... 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
若是在函数内部引用了一个和全局变量同名的变量,且不是从新定义、从新赋值(其实python中没有变量声明的概念,只有赋值的概念),那么函数内部引用的是全局变量。
例如,下面的函数g()中,print函数中的变量x并未在g()中单独定义或赋值,因此这个x引用的是全局变量x,它将输出值3。
x=3 def g(): print(x) # 引用全局变量x
若是函数内部从新赋值了一个和全局变量名称相同的变量,则这个变量是本地变量,它会掩盖全局变量。注意是掩盖而非覆盖,掩盖的意思是出了函数的范围(函数退出),全局变量就会恢复。或者换句话说,在函数内部看到的是本地变量x=2
,在函数外部看到的是全局变量x=3
。
例如:下面的g()中从新声明了x,这个x称为g()的本地变量,全局变量x=3
暂时被掩盖(固然,这是对该函数来讲的掩盖)。
x=3 def g(): x=2 # 定义并赋值本地变量x print(x) # 引用本地变量x
python是一种解释性语言,读一行解释一行,读了下一行就忘记前一行(详细见下文)。因此在使用变量以前必须先进行变量的定义(声明)。
例以下面是错误的:
def g(): print(x) x=3 g()
错误信息:
UnboundLocalError: local variable 'x' referenced before assignment
这个很好理解,可是下面和同名的全局变量混合的时候,就不那么容易理解了:
x=1 def g(): print(x) x=2 g()
这里也会报错,而不是输出x=1或2。
这里须要解释一下,虽然说python是逐行解释的。但每一个函数属于一个区块,这个区块范围是一次性解释的,并不会读一行忘记一行,而是一直读,读完整个区块再解释。因此,读完整个g()区块后,首先就记住了从新定义了本地变量x=2
,因而g()中全部使用变量x的时候,都是本地变量x,因此print(x)中的x也是本地变量,但这违反了使用变量前先赋值的规则,因此也会报错。
所以,在函数内修改和全局变量同名的变量前,必须先修改,再使用该变量。因此,上面的代码中,x=2
必须放在print的前面:
x=1 def g(): x=2 print(x) g()
因此,对于函数来讲,也必须先定义函数,再调用函数。下面是错误的:
g() def g(): x=2 print(x)
报错信息:
NameError: name 'g' is not defined
可是下面的代码中,f()中先调用了g(),而后才定义g(),为何能执行呢:
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
实际上并不是是先调用了g(),python解释到def f()区块的时候,只是声明这一个函数,并不是调用这个函数,真正调用f()的时候是在def g()
区块的后面,因此其实是先声明完f()和g()以后,再调用f()和g()的。
但若是把f()放在def f()
和def g()
的中间,将会报错,由于调用f()函数的时候,def g()
还没解释到,也就是说g()尚未声明。
x=1 def f(): x=3 g() print("f:",x) f() # 报错 def g(): print("g:",x)
更容易犯错的一种状况是边赋值,边使用。下面的代码是错误的:
x=3 def f1(): x += 3 print(x) f1()
由于x += 3
也是赋值操做,函数内部只要是赋值操做就表示声明为本地变量。它首先计算x=x+3
右边的x+3
,而后将结果赋值给左边的变量x,但计算x+3
的时候变量x并未定义,因此它是错误的。错误信息:
UnboundLocalError: local variable 'x' referenced before assignment
关于python中的全局变量:
默认状况下,下面f()中的x变量是全局变量:
x=2 def f(): print(x) f() # 输出2
默认状况下,下面f()中的x变量是本地变量:
x=2 def f(): x=3 print(x) f() # 输出3 print(x) # 输出2
若是想要在def的内部修改全局变量,就须要使用global关键字声明变量:
x=2 def f(): global x x=3 print(x) f() # 输出3 print(x) # 输出3
global能够声明一个或多个变量为全局变量,多个变量使用逗号隔开,也能够声明事先不存在的变量为全局变量:
x=2 def f(): global x,y x,y = 3,4 print(x,y) f() print(x,y)
不能global中直接赋值。因此下面的是错的:
global x=2
注意,global不是声明变量,在变量赋值以前,变量是必定不存在的,就算是被global修饰了也同样不存在,因此下面的代码是错的。实际上,global有点相似于声明变量的名称空间,而非变量。
x=2 def f(): global x,y print(y)
报错信息:
NameError: name 'y' is not defined
必须在print(y)以前(不能是以后)加上y的赋值语句,才表示它的存在。
x=2 def f(): global x,y y=3 print(y)
global修饰的变量必须在它的赋值以前,因此下面的是错的,由于y=2首先将它声明为本地变量了。
def f(): y=2 global y
考虑下面这个问题:
x=2 def f(): global x x=3 def g(): global x x=4 f()或g() print(x)
这时,函数f()和g()的调用顺序决定了print输出的全局变量x的值。由于全局变量是共享的,若是多线程同时执行这段代码,就不知道是谁先谁后修改,致使print(x)的值随机性。这是多线程不安全特性。所以,若是容许,应尽可能不要将函数内部的变量修饰为全局变量。
python中一个文件一个模块,在模块1中能够导入模块2中的属性(例如全局变量)。
例如,b.py文件中:
x=3
a.py文件中:
import b print(b.x) b.x=4
上面的a.py中导入了b.py模块,经过b.x
能够访问、修改这个来自于b.py中的全局变量x。
这是极不安全的,由于谁也不知道是否有其余的模块也在修改b.x
。
因此,没有人会去直接修改其它模块的属性,若是要修改,基本上都会经过相似面向对象中的setter函数进行修改。只需在b.py中定义一个函数,之后在其它文件中使用这个函数修改便可。
b.py文件中:
x=3 def setx(n) global x x=n
a.py文件中:
import b b.setx(54) # 将b.x变量设置为54
上面经过import导入模块文件,就能够获取这个模块中属性的访问权。实际上,也能够在当前模块文件中使用import mod_name
导入当前模块,其中mod_name
为当前文件名,这样就能够在函数内部直接访问全局变量,而无需使用global关键字。
除了import mod_name
能够导入当前模块,使用sys模块的modules()函数也能够导入:sys.modules['mod_name']
。
例如,在b.py文件中:
x=3 def f(): global x x += 2 def f1(): x=4 # 本地变量 def f2(): x=4 # 本地变量 import b b.x += 2 # 全局变量 def f3(): x=4 # 本地变量 import sys glob = sys.modules['b'] glob.x += 2 # 全局变量 def test(): print("aaa",x) # 输出3 f();f1();f2();f3() print("bbb",x) # 输出9
在a.py文件中:
import b b.test()
当函数进行嵌套的时候,内层函数的做用域是最内层的,它的外层是外层函数的做用域。内层函数和外层函数的关系相似于本地做用域与全局做用域的关系:
例如,下面的嵌套代码中,f2()中print(x,y)
的x是属于外层函数f1()的本地变量,而y则是属于内层函数自身的本地变量,外层函数f1()没法访问属于内层函数的y。
x=3 def f1(): x=4 def f2(): y=5 print(x,y) f2() f1()
nonlocal语句能够修饰内层函数中的变量使其成为它上一层函数的变量。它的用法和global基本相同,修饰多个变量的时候,须要逗号隔开。但和global有一点不一样,global修饰的变量可能事先并未存在于全局做用域内,但nonlocal修饰的变量必须已经存在于上层或上上层(或更多层)函数,不能只存在于全局(见下面示例)。
例以下面的代码片断中嵌套了2次,其中f3()中的x使用nonlocal修饰,使得这个x变成它上一层做用域f2()中的x变量。
x=3 def f1(): x=4 # f1的本地变量 def f2(): x=5 # f2的本地变量 def f3(): nonlocal x # f2的本地变量 print("f3:",x) # 输出5 x=6 f3() print("f2:",x) # 被修改,输出6 f2() f1()
上面的代码将输出:
f3: 5 f2: 6
若是将上面的f2()中的x=5
删除,会如何?
x=3 def f1(): x=4 def f2(): def f3(): nonlocal x # f1()的本地 print("f3:",x) # 输出4 x=6 # 修改f1()的本地 f3() print("f2:",x) # 输出6 f2() print("f1:",x) # 输出6 f1()
注意,上面f3()中的nonlocal将x修饰为f1()的本地变量,由于f3()的上一层f2()中没有变量x,因此f2()继承了f1()的变量x,使得f3()修改上一层f2()中的变量,等价于修改f1()中的变量x。
但若是把f1()中的x=4
也删除,那么将报错,由于nonlocal没法将变量修饰为全局范围。
因此,nonlocal默认将内层函数中的变量修饰为上一层函数的做用域范围,若是上一层函数中不存在该变量,则修饰为上上层、上上上层直到顶层函数,但不能修饰为全局做用域范围。
一样的,只要在内层函数中赋值,就表示声明这个变量的做用域为内层函数做用域范围。因此,下面的代码是错误的:
x=3 def f1(): x=4 def f2(): print(x) x=3 f2() f1()
下面的代码也是错的:
x=3 def f1(): x=4 def f2(): x += 3 print(x) f2() f1()
错误信息:
UnboundLocalError: local variable 'x' referenced before assignment
至于缘由,前文已经解释的很清楚。
在之前的版本中,尚未nonlocal关键字,这时若是要保存外层函数的变量,就须要使用函数参数默认值的方式定义内层函数。
x=3 def f1(): x=4 def f2(x=x): x += 3 print("f2:",x) x=5 f2() print("f1:",x) f1()
输出:
f2: 7 f1: 5
上面的f2(x=x)
中,等号右边的x来自于f1()中x=4
,而后将其赋值给f2()的本地做用域变量x。注意,python的做用域是词法做用域,函数区块的定义位置决定了它看到的变量。因此,尽管调用f2()以前再次对x进行了赋值,f2()函数调用时,f2(x=x)
等号右边的x早已经赋值给左边的本地变量x了。它们的关系以下图所示:
通常来讲,函数嵌套都只用于闭包(工厂函数),并且是结合匿名函数(lambda)实现的闭包。其它时候,函数嵌套通常均可以改写为非嵌套模式。
例如,下面的嵌套函数:
def f1(): x=3 def f2(): nonlocal x print(x) f2() f1()
能够改写为:
def f1(): x=3 f2(x) def f2(x): print(x) f1()
当函数位于循环结构中,且这个函数引用了循环控制变量,那么结果可能会出乎意料。
原本以匿名函数(lambda)来解释更清晰,但由于还没有介绍匿名函数,因此这里采用命名函数为例。
下面的代码中,将5个函数做为列表的元素保存到列表list1中。
def f1(): list1 = [] for i in range(5): def n(x): return i+x list1.append(n) return list1 mylist = f1() for i in mylist: print(i) print(mylist[0](2)) print(mylist[2](2))
结果:
<function f1.<locals>.n at 0x02F93660> <function f1.<locals>.n at 0x02F934B0> <function f1.<locals>.n at 0x02F936A8> <function f1.<locals>.n at 0x02F93738> <function f1.<locals>.n at 0x02F93780> 6 6
从结果中能够看到mylist[0](2)
和mylist[2](2)
的执行结果是同样的,不只如此,mylist[N](2)
的结果也全都同样。换句话说,保存到列表中的各个函数n()中所引用的循环控制变量"i"并无由于循环的迭代而改变,并且列表中全部函数保存的i的值都是循环的最后一个元素i=4
。
(注:对于此现象,各语言基本都是如此,本节稍做解释,真正的本质缘由在本文的最后一节作了额外的补充解释代码块细述)。
先看下面的例子:
def f1(): for i in range(5): def n(): print(i) return n f1()()
结果输出4。可见,print(i)
的值并无随循环的迭代过程而改变。
究其缘由,是由于def n()
只是函数的声明,它不会去查找i的值是多少,因此不会将i的值替换到函数n()的i变量,而是直接保存变量i的地址,当循环结束时,i指向最后一个元素i=4的地址。
当开始调用n()的时候,即f1()()
,才会真正开始查找i的值,这时候i指向的正是i=4。
这就像下面的代码同样,在尚未开始调用f()的时候,f()内部的x一直都只是指向它所看见的变量x,而这个x是全局做用域范围。当真正开始调用f()的时候,才会去定位x的指向。
x=3 def f(): print(x)
回到上面循环中的嵌套函数,若是要保证循环的迭代能做用到其内部的函数中,能够采用默认参数值的方式进行赋值:
def f1(): list1 = [] for i in range(5): def n(x,i=i): return i+x list1.append(n) return list1
上面def n(x,i=i)
中的i=i
是设置默认参数值,等号右边的i是函数声明时就查找并替换完成的,因此每次循环迭代过程当中,等号右边的i都不一样,等号左边的参数i的默认值就不一样。
python的做用域是词法做用域,这意味着函数的定义位置决定了它所看见的变量。除了词法做用域,还有动态做用域,动态做用域意味着函数的调用位置决定了它所看见的变量。关于词法、动态做用域,本文很少作解释,想要了解的话,能够参考一文搞懂:词法做用域、动态做用域、回调函数、闭包
下面是本文开头的问题:
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
对于python的这段代码来讲,这里有两个值得注意的地方:
第一个问题在前文已经解释过了,再解释一遍:虽然f()里面有g()的调用语句,但def f()
只是声明,但在调用f()以前,是不会去调用g()的。因此,只要f()的调用语句在def g()
以后,就是正确的。
第二个问题,python是词法做用域,因此:
def f()
,在此期间会建立一个本地变量x,而且print("f:",x)
中的x指向这个本地变量;g()
,在此期间,g()的定义语句不在f()内部,而是在全局范围,因此它看见的是x是全局x,因此print("g:",x)
中的x指向全局变量x;当调用f()的时候,执行到g()时,g()中所指向的是全局范围的x,而非f()段中的x。因此,输出1。
再看一个嵌套在函数内部的示例:
x=3 def f1(): x=4 def f2(): print(x) x=5 f2() f1() # 输出5
这里的问题是f2()中的print为何不输出4,而是输出5。
其实也很容易理解,由于def f2()
是定义在f1()内部的,因此f2()看见的x是f1()中的x,也就是说print(x)
中的x指向的是f1()中的x。但在调用f2()以前,从新赋值了x=5
,等到调用f2()的时候,根据x的指向,将找到新的x的值。
也就是说,前面的示例中,有两个独立的变量x:全局的和f()本地的。后面这个示例中只有一个变量x,属于f()。
代码块可使得一段python代码做为一个单元、一个总体执行。如下是 官方手册 的描述。
A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the ‘-c’ option) is a code block. The string argument passed to the built-in functions eval() and exec() is a code block.
因此,有如下几种类型的代码块:
代码块的做用是组织代码,同时意味着退出代码区块范围就退出了做用域范围。例如退出函数区块,就退出了函数的做用域,使得函数内的本地变量没法被函数的外界访问。
此外,python是解释性语言,读一行解释一行,这意味着每读一行就忘记前一行。但实际上更严格的说法是读一个代码块解释一个代码块,这意味着读代码块中的内容时,是暂时记住属于这个代码块中所读内容的,读完整个代码块后再以统筹的形式解释这个代码块。
先说明读一行解释一行的状况,也就是每一行都属于一个代码块,这个只能经过python的交互式工具idle工具来测试:
>>> x=2000 >>> y=2000 >>> x is y False >>> x=2000;y=2000 >>> x is y True
理论上分号是语句的分隔符,并不会影响结果。但为何第一个x is y
为False,而第二个x is y
为True?
首先分析第一个x is y
。因为交互式工具idle中每个命令都是一个单独的语句块,这使得解释完x=2000
后马上就忘记了2000这个数值对象,同时忘记的还有x变量自己。而后再读取解释y=2000
,由于不记得刚才解释的x=2000
,因此会在内存中从新建立一个数值结构用来保存2000这个数值,而后用y指向它。换句话说,x和y所指向的2000在内存中是不一样的数据对象,因此x is y
为False。
下面的x is y
返回True:
>>> x=2000;y=2000 >>> x is y True
由于python按行解释,一个命令是一个代码块。对于x=2000;y=2000
,python首先读取这一整行,发现x和y的数值对象都是2000,因而作个简单优化,等价于x,y=2000,2000
,这意味着它们属于一个代码块内,因为都是2000,因此只会在内存中建立一个数据对象,而后x和y都引用这个数据对象。因此,x is y
返回True。
idle工具中每一个命令都是独立的代码块,可是py文件倒是一个完整的代码块,其内还能够嵌套其它代码块(如函数、exec()等)。因此,若是上面的分行赋值语句放在py文件中,获得的结果将是True。
例如:
x = 2000 y = 2000 print(x is y) # True def f1(): z=2000 z1=2000 print(x is z) # False print(z is z1) # True f1()
python先读取x=2000
,并在内存中建立一个属于全局做用域的2000数据对象,再解释y=2000的时候,发现这个全局对象2000已经存在了(由于x和y同处于全局代码块内),因此不会再额外建立新的2000对象。这里反映出来的结果是"同一个代码块内,虽然仍然是读一行解释一行,但在退出这个代码块以前,不会忘记这个代码块中的内容,并且会统筹安排这个代码块"。
同理def f1()
内的代码块,由于z是本地做用域的变量,更标准的是处于不一样代码块内,因此会在本地做用域内存区建立新的数据对象2000,因此x is z
返回False。根据前面的解释,z1 is z
返回True。
再回顾前文屡次出现的一个异常:
x = 3 def f1(): print(x) x=4 f1()
报错信息:
UnboundLocalError: local variable 'x' referenced before assignment
当执行到def语句的时候,由于def声明函数,函数体是一个代码块,因此按照代码块的方式读取属于这个代码块中的内容。首先读取print(x)
,但并不会直接解释,而是会记住它,并继续向下读取,因而读取x=4,这意味着x是一个本地变量。而后统筹安排整个代码块,将print(x)的x认为是本地变量而非全局变量。注意,直到def退出的时候都尚未进行x的赋值,而是记录了本地变量x,赋值操做是在函数调用的时候进行的。当调用函数f()的时候,发现print(x)中的x是本地变量,但由于尚未赋值,因此报错。
可是再看下面的,为何又返回True?
>>> x=256 >>> y=256 >>> x is y True
由于Python在启动的时候就在内存中预先为经常使用的较小整数值(-5到256)建立好了对象,由于它们使用的很是频繁(有些在python的内部已经使用了)。因此,对于这个范围内的整数,都是直接引用,不会再在内存中额外建立新的数值对象,因此x is y
老是返回true。甚至,这些小值整数能够跨做用域:
x = 3 def f1(): y=3 print(x is y) # True f1()
再看前文循环内的函数的问题。
def f1(): for i in range(5): def n(): print(i) return n f1()()
前面对现象已经解释过,内部函数n()中print(i)的i不会随循环的迭代而改变,而是固定的值i=4。
python首先解释def f1()
的代码块,会记录属于这个代码块做用域内的变量i和n,但i和n都不会赋值,也就是说暂时并不知道变量n是一个函数变量。
同理,当须要解释def n()
代码块的时候,将记住这个代码块涉及到的变量i,只不过这个变量i是属于外层函数的,但无论如何,这个代码块记住了i,且记住了它是外部函数做用域的。
注意,函数的声明过程当中,全部涉及到变量i的做用域内都不会对i进行赋值,仅仅只是保存了这个i变量名,只有在调用函数的时候才会进行赋值操做。
当开始调用f1()的时候,开始执行函数体中的代码,因而开始循环迭代,且屡次声明函数n()
,每一次迭代生成的n()都会让原先已记录的变量n指向这个新声明的函数体(至关于赋值的操做,只不过是变量n引用的对象是函数体结构,而不是通常的数据对象),因为只是在循环中声明函数n(),并无进行调用,因此不会对n()中的i进行赋值操做。并且,每次循环迭代都会让变量n指向新的函数体,使得先前迭代过程当中定义的函数被丢弃(覆盖),因此最终只记住了最后一轮循环时声明的函数n(),而且i=4。
当调用f1()()时,表示调用f1()中返回的函数n(),直到这个时候才会对n()内的i进行赋值,赋值时将搜索它的外层函数f1()做用域,发现这个做用域内的i指向内存中的数值4,因而最终输出4。
再看下面的代码:
def f1(): for i in range(5): def n(): print(i) n() return n f1()
输出结果:
0 1 2 3 4
调用f1()的时候,执行循环的迭代,每次迭代时都会调用n(),意味着每次迭代都要对n()中的i进行赋值。
另外注意,前面说过,函数的默认参数是在函数声明时进行赋值的,因此下面的列表L中每一个元素所表明的函数,它们的变量i都指向不一样的数值对象。
def f1(): L = [] for i in range(5): def n(i=i): print(i) L.append(n) return L f1()[0]() f1()[1]() f1()[2]() f1()[3]() f1()[4]()
执行结果:
0 1 2 3 4