Python作用域和名字空间

名字空间是Python的一个非常核心的概念,而作用域则是跟名字空间紧密联系的一个概念。Python有3个独立的名字空间:local、global、builtin。在Python中,类、函数、module都对应着一个独立的名字空间。名字空间是程序运行时的概念,而作用域则是与之对应的静态的东西,也就是说它是由源程序的文本决定的。由于我们经常要对作用域中的名字做直接访问(这类行为我们称为名字引用),所以Python中就要有一定规则,这个规则叫做“最内嵌套作用域规则”。看下面这个简单的例子:

        a = 1
         
        def f():
            a = 2
            print "a in f: ", a
            def g():
                a = 3
                print "a in g: ", a
            return g
         
        def h():
            print "a in h: ", a
            def h2():
                print "a in h2: ", a
            return h2
         
        print "global a: ", a
        func = f()
        func()
        func2 = h()
        func2() 

        """
        程序的输出结果是:
        global a:  1
        a in f:  2
        a in g:  3
        a in h:  1
        a in h2:  1 
        """
        

可以看出,由一个赋值语句绑定的名字在这个赋值语句的作用域是可见的(从“global a: 1”得出),而且在其内部嵌套的每个作用域也是可见的(从“a in h: 1,a in h2: 1”得出),除非它被嵌套于内部的,引进同样名字的另一条赋值语句所遮蔽(从“a in f: 2,a in g: 3”得出)。

这个例子是很容易理解,因为像C/C++等语言也符合这个规则,所以不用深究,想当然地也不会出错。值得注意的是,Python没有C/C++那种“块作用域”的概念,也就是说,类似这样的C程序:

        #include<stdio.h>
        int main(){
            if(1){
                int a = 1;
            }
            printf("%d\n", a);
            return 0;
        } 
        

程序会提示a未声明,而在Python程序:

        if True:
            a = 1
        print a 
        

程序则会正常地输出a的值为1。那是因为if这样的语句在Python中不作为一个新的Code Block,也就没用其相应的名字空间和作用域,所以“a = 1”这样的绑定语句其实跟下面“print a”是在同一个作用域里面的,它的作用是使a这个变量在当前作用域及其内部嵌套的每个作用域可见。但是可见不见得一定能够引用成功,可以看看这一个例子(书上的例子,这个例子是为再下面的例子做铺垫的):

        a = 1 
         
        def g():
            print a
         
        def f():
            print a  # [1]
            a = 2  # [2]
            print a  # [3]
         
        g()
        f() 
        

包括我在内的很多Python初学者,看到这个程序,会觉得[1]肯定是输出1,[3]会输出2。但事实上,[1]这里会抛出一个异常,说a未赋值先引用。之前说过,只要出现赋值语句(除了“=”外,“def”、“class”、“import”等等也是赋值语句),那么该赋值的符号在其作用域是可见的,也就说在[1]这个地方,“print a”已经看到局部里面的“a”,而不是找全局的那个“a”,但是“a”的赋值却是在[2]这个地方才出现,所以[1]这里实际上是引用了一个可见的但未赋值的局部变量。

说了几个例子,似乎对Python作用域有了一定了解。下面是一个更进阶的例子,之前学习decorator的时候,遇到过这样的一个程序(为了简单突出地说明问题,做了删减和修改):

        # test1.py
        def f():
            a = [0]
            def g():
                a[0] = a[0] + 1
                print a[0]
            return g
        f()() 
        

我很好奇,为什么a要是一个列表,我尝试把a改为一个整数:

        # test2.py
        def f():
            a = 0
            def g():
                a = a + 1
                print a
            return g
        f()() 
        

这个时候,得到的是一个异常:UnboundLocalError: local variable 'a' referenced before assignment。当时觉得非常诡异,为什么“a”会出现这个异常而“a[0]”这样的引用又不会呢,这里就算搬出“最内嵌套作用域规则”,似乎也不好解释,所以需要分析它的字节码。首先,我们分析错误的那个程序[test2.py],在“def g():”下面,加入“dis.dis(g)”这个语句(dis是一个Python提供的反汇编Python源代码得到字节码的一个工具,需要import dis),得到下面这样一组字节码(经过删减加工,混入了Python源码作对比):

a = a + 1:
6        13 LOAD_FAST                0 (a)
         16 LOAD_CONST               1 (1)
         19 BINARY_ADD          
         20 STORE_FAST               0 (a)
print a:
7        23 LOAD_FAST                0 (a)
         26 PRINT_ITEM          
         27 PRINT_NEWLINE   
        

对于“a = a + 1”,它生成了4条字节码:LOAD_FAST、LOAD_CONST、BINARY_ADD、STORE_FAST。LOAD_FAST要做的是把一个对局部变量“a”的引用压到栈顶。LOAD_CONST把常量“1”压到栈顶。BINARY_ADD把栈顶的两个元素(一个最顶,另一个被它“压着”)弹出来,作加法,然后把结果又压到栈顶。STORE_FAST把栈顶元素存储到局部变量“a”。在这里,我们可以看到,决定“a”是一个局部变量的行为,在编译的时候就落实了,而运行时,执行LOAD_FAST,才发现“a”这个局部变量不存在。这里可能有疑问,为什么Python要在编译时把“a”理解成一个局部变量呢?回想我们刚才例子,其实答案呼之欲出了。Python说了,只要在一个作用域里出现赋值语句,那么这个被赋值的符号就在这个作用域里可见。明显,“a = a + 1”是一个赋值语句,“a”又出现在左值,所以编译器把“a”这个符号理解成局部变量,在执行“a + 1”的时候,自然会从局部变量里找这个值,再进行加法。

解决了这个问题,我们又看看为什么a[0]这种形式又可以呢。同样地,对[test1.py]进行反汇编操作:

a[0] = a[0] + 1
6        13 LOAD_DEREF               0 (a)
         16 LOAD_CONST               1 (0)
         19 BINARY_SUBSCR       
         20 LOAD_CONST               2 (1)
         23 BINARY_ADD          
         24 LOAD_DEREF               0 (a)
         27 LOAD_CONST               1 (0)
         30 STORE_SUBSCR        
print a[0]
7        31 LOAD_DEREF               0 (a)
         34 LOAD_CONST               1 (0)
         37 BINARY_SUBSCR       
         38 PRINT_ITEM          
         39 PRINT_NEWLINE        
        

对于“a[0] = a[0] + 1”,多了三条新的字节码:LOAD_DEREF、BINARY_SUBSCR、STORE_SUBSCR。LOAD_DEREF在这里做的是往栈顶压入一个对cell[i]的引用(cell是一个被内嵌函数引用的局部变量名的tuple,在这里只有一个“a”,所以i为0),BINARY_SUBSCR实现的是“TOS = TOS1[TOS]”(解释一下,TOS指栈顶元素,TOS1指紧接着栈顶的元素,由于是先压“a”,再压“0”,所以TOS是“0”,TOS1是“a”,而“TOS = TOS1[TOS]”就是先后弹出“0”和“a”,然后吧“a[0]”的值压回栈顶)。STORE_SUBSCR实现的是“TOS1[TOS] = TOS2”(类似地,TOS2表示TOS1下面的元素,这时TOS、TOS1、TOS2分别是符号“0”,符号“a”,a[0]+1的值),就是把栈顶最上面的3个元素弹出来,然后调用PyObject_SetItem,把“a[0]”赋值为“a[0]+1”。有了对这三个新指令的描述,再用一个运行时栈的示意图来描述,就更清楚了:

LOAD_DEREF 0  

           |---|                    
           | a |                                            
           |---|                                           
           | ? |                                              
                                   
LOAD_CONST 1  

           |---|
           | 0 |
           |---|
           | a |
           |---|

BINARY_SUBSCR     

               |----|
               |a[0]|
               |----|
               | ?  |

LOAD_CONST 2     

              |----|
              | 1  |
              |----|
              |a[0]|
              |----|

BINARY_ADD      

             |------|
             |a[0]+1|
             |------|
             |   ?  |

LOAD_DEREF 0    

             |------|
             |   a  |
             |------|
             |a[0]+1|
             |------|

LOAD_CONST 1    

             |------|
             |   0  |
             |------|
             |   a  |
             |------|
             |a[0]+1|
             |------|

STORE_SUBSCR    

             |------|        ---> PyObject_SetItem(a, 0, a[0] + 1)
             |   ?  |
             |------| 

        

执行完STORE_SUBSCR后,栈顶已经没有了“a”、“0”、“a[0]+1”这些东西了(注意,紧接着STORE_SUBSCR后它们还在原来的地方,只是Python源码用一个叫stack_pointer的东西表示栈顶,stack_pointer在STORE_SUBSCR后把指针移到“a[0]+1”下面,所以看起来它们不在栈里了,至于接下来它们被怎样覆盖,这是不管的,因为它们不再属于栈)原来的“a[0]”也被替换成“a[0] + 1”。下面的“print a[0]”,原理很简单,就不详细解释了。

讲到这里,感觉这种情况下,要用“a[0]”代替“a”,还是有点别扭的,毕竟我们会经常这样用。例如要设置一个计数器,统计闭包函数的调用次数,这时就要用“a[0]”了,而不能用“a”。而由于“a”也不是全局变量,又不能通过声明“global a”来做,很tricky。不过听说Python 3引入了一个新的关键字“nonlocal”,有了这个,就可以写下面的语句了:

            def f():
                a = 0
                def g():
                    nonlocal a
                    a = a + 1
                    print a
                return g
             
            func = f()
            func()
            func() 
        

程序会先后正确返回1和2。这样子的话,进行闭包的某些操作就比以前更方便和直观了。在这里,我们只介绍了名字引用的相关规则和实例,Python名字空间里还有一种叫做属性引用,比较简单,就是用“.”来对对象的属性进行访问,参照着“最内嵌套作用域规则”,是很好理解的,这也不作介绍了。