Python中的LEGB规则

目标

  • 命名空间和作用域——Python从哪里查找变量名?
  • 我们能否同时定义或使用多个对象的变量名?
  • Python查找变量名时是按照什么顺序搜索不同的命名空间?

命名空间与作用域的介绍

命名空间

大约来说,命名空间就是一个容器,其中包含的是映射到不同对象的名称。你可能已经听说过了,Python中的一切——常量,列表,字典,函数,类,等等——都是对象。

这样一种“名称-对象”间的映射,使得我们可以通过为对象指定的名称来访问它。举例来说,如果指定一个简单的字符串a_string = "Hello string",我们就创建了一个对象“Hello string”的引用,之后我们就可以通过它的名称a_string来访问它。

我们可以把命名空间描述为一个Python字典结构,其中关键词代表名称,而字典值是对象本身(这也是目前Python中命名空间的实现方式),如:

a_namespace = {'name_a':object_1, 'name_b':object_2, ...}

现在比较棘手的是,我们在Python中有多个独立的命名空间,而且不同命名空间中的名称可以重复使用(只要对象是独一无二的),比如:

a_namespace = {'name_a':object_1, 'name_b':object_2, ...}
b_namespace = {'name_a':object_3, 'name_b':object_4, ...}

举例来说,每次我们调用for循环或者定义一个函数的时候,就会创建它自己的命名空间。命名空间也有不同的层次(也就是所谓的“作用域”),我们会在下一节详细讨论。

作用域

在上一节中,我们已经学习到命名空间可以相互独立地存在,而且它们被安排在某个特定层次,由此引出了“作用域”的概念。Python中的“作用域”定义了一个“层次”,我们从其中的命名空间中查找特定的“名称-对象”映射对。

举例来说,我们来考虑一下下面的代码:

i = 1

def foo():
    i = 5
    print(i, 'in foo()')

print(i, 'global')

foo()
1 global
5 in foo()

我们刚刚两次定义了变量名i,其中一次是在函数foo内。

foo_namespace = {'i':object_3, ...}
global_namespace = {'i':object_1, 'name_b':object_2, ...}

这样的话,如果我们要打印变量i的值,Python如何知道应该搜索哪个命名空间呢?到此LEGB规则就开始起作用了,我们将在下一节进行讨论。

提示:

如果我们想要打印出全局变量与局部变量的字典映射,我们可以使用函数globals()locals():

#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())
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为自身保留的特殊名称。

因此,如果某个name:object映射在局部(local)命名空间中没有找到,接下来就会在闭包作用域(enclosed)进行搜索,如果闭包作用域也没有找到,Python就会到全局(global)命名空间中进行查找,最后会在内建(built-in)命名空间搜索(注:如果一个名称在所有命名空间中都没有找到,就会产生一个NameError)。

注意:

命名空间也可以进一步嵌套,例如我们导入模块时,或者我们定义新类时。在那些情形下,我们必须使用前缀来访问那些嵌套的命名空间。我用下面的代码来说明:

import numpy
import math
import scipy

print(math.pi, 'from the math module')
print(numpy.pi, 'from the numpy package')
print(scipy.pi, 'from the scipy package')
3.141592653589793 from the math module
3.141592653589793 from the numpy package
3.141592653589793 from the scipy package

(这也就是为什么当我们通过“from a_module import *”导入模块时需要格外小心,因为这样会把变量名加载到全局命名空间中,而且可能覆盖已经存在的变量名)

1. LG - 局部与全局作用域

例 1.1

作为热身练习,我们先忘记LEGB规则中的外围函数(E)和内建(B)两个作用域,只考虑LG——局部与全局作用域。

下面的代码输出是怎样的?

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,它将会在上一层的全局作用域(G)中进行搜索,其中已经定义了a_var。

例 1.2

现在,我们在全局作用域和局部作用域中都定义变量a_var。你是否知道下面代码的结果?

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 outside a_func() ]
c)

global value [ a_var inside a_func() ]  
global value [ a_var outside a_func() ]

解析:

当我们调用a_func()时,首先会在局部作用域中查找a_var,因为a_var已经在局部作用域进行了定义,所以它在局部作用域所赋的值就会被打印出来。注意这并不会影响全局变量,因为是在不同的作用域当中。

不过,如果使用global关键字,也可以修改全局变量。如下所示:

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() ]')
global value [ a_var outside a_func() ]
local value [ a_var inside a_func() ]
local value [ a_var outside a_func() ]

但是我们必须注意顺序:如果我们没有明确地告诉Python我们要使用的是全局作用域,而是直接尝试修改变量值的话,就很容易产生UnboundLocalError。(记住,赋值操作的右半部分是先执行的)

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()
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()
---------------------------------------------------------------------------
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 - 局部,闭包与全局作用域

现在我们引入外围函数(E)作用域的概念。根据顺序“Local-> Enclosed -> Global”,你能否猜出下面代码的输出结果?

例 2.1

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(),该函数也定义了一个名称为a_var的变量。在inner()内的print()函数首先在局部作用域内搜索(L->E),因此会打印出在局部作用域内所赋的值。

类似于上一节所说的global关键字,我们也可以在内部函数中使用nonlocal关键字来明确地访问外部(外围函数)作用域的变量,也可以修改它的值。

注意nonlocal关键字是在Python 3.x才新加的,而且在Python 2.x中没有实现(目前还没有)。

a_var = 'global value'

def outer():
       a_var = 'local value'
       print('outer before:', a_var)
       def inner():
           nonlocal a_var
           a_var = 'inner value'
           print('in inner():', a_var)
       inner()
       print("outer after:", a_var)
outer()
outer before: local value
in inner(): inner value
outer after: inner value

3. LEGB - 局部,外围,全局,内建

为了完整理解LEGB规则,我们来学习内建作用域。在这里,我们定义“自己的”长度函数,碰巧跟内建的len()函数同名。如果我们执行下面的代码,你认为输出结果是什么?

例 3

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来定义我们自己的长度函数是没有问题的(这只是为了示范,不是必须的)。我们在Python中按照L -> E -> G -> B层次进行搜索,a_func()函数在尝试搜索内建(B)命名空间之前,首先会在全局作用域(G)中发现len()。

自我评估练习

现在我们已经完成了一些练习,我们来快速地检查一下效果。那么再问一次:下面的代码会输出什么?

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)
a is local variable
called my len() function: 14
6
a is global

总结

我希望这个简短的教程能有助于理解Python中的一个基本概念,即使用LEGB规则的作用域解析顺序。我鼓励你明天重新查看一下这些代码片段(作为一个小的自我评估),检查一下你是否可以准确预测所有的输出值。

经验法则

在实际中,在函数作用域内修改全局变量通常是个坏主意,因为这经常造成混乱或者很难调试的奇怪错误。如果你想要通过一个函数来修改一个全局变量,建议把它作为一个变量传入,然后重新指定返回值。

例如:

a_var = 2

def a_func(some_var):
    return 2**3

a_var = a_func(a_var)
print(a_var)

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循环变量“泄漏”到全局命名空间

与其它一些编程语言不同,Python中的for循环会使用它所在的作用域,而且把它所定义的循环变量加在后面。

for a in range(5):
    if a == 4:
        print(a, '-> a in for-loop')
print(a, '-> a in global')
4 -> a in for-loop
4 -> a in global

如果我们提前在全局命名空间中明确定义了for循环变量,也是同样的结果!在这种情况下,它会重新绑定已有的变量:

b = 1
for b in range(5):
    if b == 4:
        print(b, '-> b in for-loop')
print(b, '-> b in global')
4 -> b in for-loop
4 -> b in global

不过,在Python 3.x中,我们可以使用闭包来防止for循环变量进入全局命名空间。下面是一个例子(在Python 3.4中执行):

i = 1
print([i for i in range(5)])
print(i, '-> i in global')
[0, 1, 2, 3, 4]
1 -> i in global

为什么我要强调“Python 3.x”?因为在Python 2.x下执行同样的代码,打印结果是:

4 -> i in global

这是因为在Python 3.x中所做的一个变化,在What’s New In Python 3.0中有如下描述:

“列表推导式不再支持 

[... for var in item1, item2, ...]

这样的语法形式,取而代之的是

 [... for var in (item1, item2, ...)] 

。也要注意列表推导式有不同的语义: 它们更接近于一个 list()构造器内的生成器表达式的语法糖,特别是循环控制变量不再泄漏到外围作用域中。”

 

 

posted @ 2019-11-26 15:42  sin涛涛  阅读(234)  评论(0编辑  收藏  举报