原文链接:http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html
本文是为了说明python 的命名空间和LEGB规则下变量的解析范围的一篇入门文章。下面的部分会提供一些简短的代码块及相关的说明和配图,你可以很方便的阅读这份入门手册,但我更推荐你能亲自运行下面的代码。
目的
命名空间和作用域:python在哪里寻找变量名
我们可以在多个对象中同时定义或重用变量名么?
python在寻找变量名的命名空间时是按照怎样的顺序?
命名空间及作用域的介绍
命名空间
不准确的说命名空间是名称映射到对象的容器。你可能曾经听过在python中万物皆对象,比如迭代器、列表、字典、函数、类等等都是对象。这样的一种名称到对象的映射允许我们通过定义过的变量名访问一个对象。例如我们定义一个简单的字符串如,a_string = "Hello string",我们创建了一个到“Hello string” 字符串的引用,从此以后我们可以通过使用变量名a_string来访问它。
可以将Python的命名空间描述为一个字典结构,key是变量名,value是对象本身(这也是命名空间怎样实现的方式),如:
a_namespace = {'name_a':object_1, 'name_b':object_2, ...}
但是现在复杂的问题是我们有很多个独立的命名空间,变量名在不同的命名空间中是可以被重用的(只有对象是唯一的,如:
a_namespace = {'name_a':object_1, 'name_b':object_2, ...} b_namespace = {'name_a':object_3, 'name_b':object_4, ...}
举个例子,当我们调用一个for循环或声明一个函数时,就会创建一个命名空间。命名空间有不同的层次结构(也叫做作用域)我们将在后文详细讨论。
作用域
在上面的部分我们学习了命名空间可以相互独立存在,并且他们是按照一定的层次结构被组织在一起的,这就引出了作用域的概念。Python中的作用域就是我们在寻找名字和对象的映射关系时的层次结构。举个例子,我们看如下的代码:
input:
i = 1 def foo(): i = 5 print(i, 'in foo()') print(i, 'global') foo()
output:
1 global 5 in foo()
在这个例子中我们定义了i变量两次,一次在foo函数中
·foo_namespace = {'i':object_3, ...}
·global_namespace = {'i':object_1, 'name_b':object_2, ...}
那么当我们在打印变量i时python是怎么知道要从哪一个命名空间中寻找呢?这种情况下Python的LEGB规则就派上了用场,详见后文。
小贴士:
如果想要打印出全局(global)本地(local)的字典映射,我们可以使用global函数和local函数:
input:
#print(globals()) # prints global namespace #print(locals()) # prints local namespace glob = 1 def foo(): loc = 5 print('loc in foo():', 'loc' in locals()) foo() print('loc in global:', 'loc' in globals()) print('glob in global:', 'foo' in globals())
output:
loc in foo(): True loc in global: False glob in global: True
LEGB规则下变量名的作用域解析
我们已经看到了多个命名空间相互间可以单独存在,并且多个命名空间可以在不同的层次结构上包含同一个变量名。作用域定义了Python在那个层次结构上为搜索变量名。那么下一个问题,Python在寻找名称与对象的映射关系时查找不同命名空间的顺序是什么?
答案是使用如下的LEGB规则:
Local -> Enclosed -> Global -> Built-in
箭头表示命名空间查找的方向:
·例如,Local级别是在一个函数或类方法里面
·Enclosed(嵌套)在其嵌套函数中,如一个函数被另一个函数装饰的情况
·Global(全局)级别的指执行脚本的最上层
·Built-in(内置)通常指Python的关键字
因此,一个名称和对象的映射如果在local级别找不到那么就会在嵌套作用域内继续搜索,同样,如果在嵌套级别也未找到,那么会在全局作用域下机继续搜索,直至内置作用域。(如果一个变量名在任何一个作用域下都没找到,那么会跑出一个NameError的异常)
注意:
命名空间是可以进一步嵌套在一起的,举个例子,如果我们引入了一些模块或定义了一个新的类,在这种情况下,我们必须使用前缀来标明嵌套在一起的命名空间,我们在下面这个代码块中解释这个概念。
input:
import numpy import math import scipy print(math.pi, 'from the math module') print(numpy.pi, 'from the numpy package')
output:
3.141592653589793 from the math module 3.141592653589793 from the numpy package 3.141592653589793 from the scipy package
这也是为什么我们在使用如“from a_module import * " 的语句来引入模块时要格外小心,因为这可能会造成引入的全局变量的名称隐式覆盖已经存在的变量名。
1.LG -Local 和Global 作用域
例1.1
一个热身的练习,让我们先忽略LEGB规则中的嵌套作用域和内置作用域仅考虑LG(本地作用域和全局作用域),下面的这段代码会输出什么呢?
input:
a_var = 'global variable' def a_func(): print(a_var, '[ a_var inside a_func() ]') a_func() print(a_var, '[ a_var outside a_func() ]')
a) raise an error
b) global value [a_var outside a _func() ]
c) global value [a_var inside a_func() ]
global value [a_var outside a_func() ]
原因:
我们调用 a_func(),首先会打印a_var值,根据LEGB规则,该函数将在本地作用域(L)内寻找a_var是否被定义,因为a_func()函数没有定义它本身的a_var,因此会在更高一级,全局作用域内查找a_var变量有没有被预先定义。
例1.2
现在,我们在LG级别中分别定义a_var变量,猜猜下面的代码会输出什么?
input:
a_var = 'global value' def a_func(): a_var = 'local value' print(a_var, '[ a_var inside a_func() ]') a_func() print(a_var, '[ a_var outside a_func() ]')
a) raises an error
b) local value [ a_var inside a_func() ]
global value [ a_var inside a_func() ]
c) global value [ a_var inside a_func() ]
global value [ a_var outside a_func()]
原因:
当我们调用a_func()方法时,首先会在本地作用域(L)内寻找a_var变量,由于a_var在本地作用域内有定义,它局部变量的赋值就会被打印出来,注意此时并不会影响到在不同作用域中的全局变量。
但是,我们也可以通过如使用global这样的关键字来修改全局变量的值,如下所示:
input:
a_var = 'global value' def a_func(): global a_var a_var = 'local value' print(a_var, '[ a_var inside a_func() ]') print(a_var, '[ a_var outside a_func() ]') a_func() print(a_var, '[ a_var outside a_func() ]')
output:
global value [ a_var outside a_func() ] local value [ a_var inside a_func() ] local value [ a_var outside a_func() ]
注意,如果我们没有指明我们将要使用的是全局作用域内的变量,并且想要改变变量的值就很容易造成UnboundLocalError(记住,右侧的表达式先被执行):
input:
a_var = 1 def a_func(): a_var = a_var + 1 print(a_var, '[ a_var inside a_func() ]') print(a_var, '[ a_var outside a_func() ]') a_func()
output:
--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-4-a6cdd0ee9a55> in <module>() 6 7 print(a_var, '[ a_var outside a_func() ]') ----> 8 a_func() <ipython-input-4-a6cdd0ee9a55> in a_func() 2 3 def a_func(): ----> 4 a_var = a_var + 1 5 print(a_var, '[ a_var inside a_func() ]') 6 UnboundLocalError: local variable 'a_var' referenced before assignment 1 [ a_var outside a_func() ]
2.LEG—本地/嵌套/全局作用域
现在我们来介绍嵌套作用域的概念。根据L->E->G规则,猜测下面代码的输出是什么?
例2.1
input:
a_var = 'global value' def outer(): a_var = 'enclosed value' def inner(): a_var = 'local value' print(a_var) inner() outer()
a) global value
b) enclosed value
c) local value
原因:
快速回想一下执行过程,我们调用了outer()方法,方法中定义了局部变量 a_var(靠近已经存在的全局变量a_var)。接下来outer()方法调用了inner()方法,inner方法也声明了一个a_var变量。inner函数中的print()方法先在本地作用域中查找,因此会打印本地作用域中的赋值。
与上文关键字global的概念类似,我们可以使用nonlocal关键字在inner函数中指定一个外部(嵌套)变量来修改它的值。注意 nonlocal 关键字在Python3.x版本之后才有。
input:
a_var = 'global value' def outer(): a_var = 'local value' print('outer before:', a_var) def inner(): nonlocal a_var a_var = 'inner valu e' print('in inner():', a_var) inner() print("outer after:", a_var) outer()
output:
outer before: local value in inner(): inner value outer after: inner value
3.LEGB-本地/嵌套/全局/内置作用域
为了总结LEGB规则,我们来看一下内置作用域。我们定义一个我们自己的计算长度的函数,并且与内置的len()函数同名,看看下面的代码输出会是什么。
例3
input:
a_var = 'global variable' def len(in_var): print('called my len() function') l = 0 for i in in_var: l += 1 return l def a_func(in_var): len_in_var = len(in_var) print('Input variable is of length', len_in_var) a_func('Hello, World!')
a) raises an error (conflict with in-built `len()` function)
b) called my len() function Input variable is of length 13
c) Input variable is of length 13
原因:
由于可以使用完全相同的名称将不同作用域中的变量映射到不同的对象,因此重用名称len来定义我们自己的length函数是完全可以的(并不推荐这样的做法)。在Python的LEGB层次结构中,函数a_func()在搜索内置作用域之前在全局范围内找到了len()函数。
自我评估练习
现在,在我们经过了一系列的练习,再看看我们现在的水平怎么样。所以,再来看看下面的代码将会打印什么?
input:
a = 'global' def outer(): def len(in_var): print('called my len() function: ', end="") l = 0 for i in in_var: l += 1 return l a = 'local' def inner(): global len nonlocal a a += ' variable' inner() print('a is', a) print(len(a)) outer() print(len(a)) print('a is', a)
结论
我希望这个简短的指南有助于理解Python使用LEGB规则的作用域解析顺序的基本概念。希望明天当你重新看上文的练习时还能正确的预测他的输出。
经验法则
在练习中,在函数内修改全局变量的值是一个坏习惯,这会造成很多难以解决的困惑和奇怪的错误。如果你想要通过函数来修改全局变量的值最好还是将其作为函数的入参,并通过函数的返回值来重新赋值。
例如:
input:
a_var = 2 def a_func(some_var): return 2**3 a_var = a_func(a_var) print(a_var)
output:
8
答案
为了防止答案在无意中被看见,答案以二进制的方式书写。为了显示实际的字符,只需要执行下述代码:
print('Example 1.1:', chr(int('01100011',2)))
print('Example 1.2:', chr(int('01100010',2)))
print('Example 2.1:', chr(int('01100011',2)))
print('Example 3.1:', chr(int('01100010',2)))
# Execute to run the self-assessment solution sol = "000010100110111101110101011101000110010101110010001010"\ "0000101001001110100000101000001010011000010010000001101001011100110"\ "0100000011011000110111101100011011000010110110000100000011101100110"\ "0001011100100110100101100001011000100110110001100101000010100110001"\ "1011000010110110001101100011001010110010000100000011011010111100100"\ "1000000110110001100101011011100010100000101001001000000110011001110"\ "1010110111001100011011101000110100101101111011011100011101000100000"\ "0011000100110100000010100000101001100111011011000110111101100010011"\ "0000101101100001110100000101000001010001101100000101001100001001000"\ "0001101001011100110010000001100111011011000110111101100010011000010"\ "1101100" sol_str =''.join(chr(int(sol[i:i+8], 2)) for i in range(0, len(sol), 8)) for line in sol_str.split('\n'): print(line)
警告:for循环变量泄露到全局命名空间
相比其他编程语言不同的是,for循环的循环变量将会在他们存在的作用域中被使用,并且在循环结束之后会仍会被保留。
input:
for a in range(5): if a == 4: print(a, '-> a in for-loop') print(a, '-> a in global')
output:
4 -> a in for-loop 4 -> a in global
这同样适用于我们在for 循环之前在全局命名空间中定义了循环变量的情况!在这种情况下,他将重新绑定现有的变量:
input:
b = 1 for b in range(5): if b == 4: print(b, '-> b in for-loop') print(b, '-> b in global')
output:
4 -> b in for-loop 4 -> b in global
然而在Python3版本中,我们可以通过使用闭包来避免循环变量泄露到全局命名空间中。如下是一个例子:(在Python3.4中执行)
input:
i = 1 print([i for i in range(5)]) print(i, '-> i in global')
output:
[0, 1, 2, 3, 4] 1 -> i in global
相同的代码在Python2版本中的运行结果是这样:
4 -> i in global
上述的变化可以追溯到Python3中的更改,在Python3中的新特性描述如下:
“List不再支持[... for var in item1, item2, ...],而是用[... for var in (item1, item2, ...)]代替。注意这种列表的表达式具有的不同的语义:这更接近于在list的构造器中的生成器表达式的语法糖,并且特别控制了循环变量不会被泄露到临近的作用域中“