作用域

作用域规定了一个变量能在什么地方使用。 python的作用域规则也很简单,可以理解为多个盒子,这些盒子是一层一层套在一起的,里面的盒子可以看到外面的盒子里的东西,而外面的盒子看不到里面盒子里的东西。这些盒子在python 中用词法来规定的。程序员自己的代码一般会在三个盒子中,自己创建出来的盒子只有以下两种:

  1. 函数盒子
  2. 类盒子

这些盒子都是装在一个文件中的,出了这些盒子,再定义变量,都是放在一个全局的盒子中,一般称作全局作用域:

  1. 全局盒子

程序是在python解释器中运行的,解释器会有一些内建的变量,这些变量不会和程序的变量放在同一个作用域下,这个作用域叫做内建作用域。 函数盒子和类盒子创建出来的作用域叫做局部作用域。

但是在python中,函数和类是可以互相嵌套的,也就是做局部作用域里面可以有新的局部作用域,这样说不好区分哪个局部作用域是哪个。程序员是一个特别喜欢给概念取名字的一个团体,所以程序员把外边那个盒子取名叫做闭包作用域(Enclosing scope),里面的还是叫做局部作用域。这里要注意的是闭包作用域只是一个相对的概念,有了嵌套才会有闭包作用域。

这些盒子的嵌套关系是这样的:

 _________________________________________________________________________
|                                                                         | 
|   内建作用域                                                              |
|   ___________________________________________________________________   |
|  |                                                                   |  | 
|  |  全局作用域                                                         |  | 
|  |   _____________________________________________________________   |  |
|  |  |                                                             |  |  |
|  |  |  闭包作用域                                                   |  |  |
|  |  |   _______________________________________________________   |  |  |
|  |  |  |                                                       |  |  |  |
|  |  |  |   局部作用域                                            |  |  |  |
|  |  |  |_______________________________________________________|  |  |  |
|  |  |_____________________________________________________________|  |  |
|  |___________________________________________________________________|  |
|_________________________________________________________________________|

程序员也是特别喜欢缩写的一个团体,所以他们把作用域的查找规则简写成了:LEGB

局部作用域(Local Scope)-> 闭包作用域(Enclosing Scope)->全局作用域(Global Scope)->内建作用域(Built-in Scope)

内部作用域可以访问到外部作用域的变量

  • 先来几个简单的例子:

局部作用域和全局作用域


In [3]:
x = 15

def func():
    print(x)

func()


15

在函数的局部作用域中可以访问到全局作用域中的变量x,但是需要注意的是这里的访问是读去x的值,下面我们试一试,如果在函数中写全局变量会怎么样


In [4]:
y = 15

def modify():
    y = 20
    print(y)
modify()
print(y)


20
15

我们可以看到,在modify函数中对y变量进行赋值,但是在全局作用域中打印y的值,发现全局变量y并没有被modify函数修改。 不是说局部作用域可以访问全局作用域中的变量么,这是怎么回事呢?

这就涉及到和python中的另外一个规则冲突的问题了。

在python中定义变量的语句和赋值语句的写法是一样的,但是这两件事情是有本质的区别的。 所谓的定义变量,是告诉解释器,我要一个新的变量,名字是y,你要给我在内存中开辟一段空间,让我把y变量的值放进去。 所谓的给变量赋值则是,hi解释器,我知道你有一个变量名字叫做y,它已经在内存中了,但是它以前的值我不想用了,你给我放一个新的值到那段内存中去。

这里就出现了问题了,当我们在modify中写一条语句“y = 20”的时候,解释器只会在局部作用域的盒子中找y变量,发现局部作用域中并没有这个变量。然后它就会认为这是一条赋值语句,接着就会在内存中开辟一段内存并在局部作用域的盒子里面放进去一个名字y。在函数中每次要操作y变量,都直接能在局部作用域中找到这个名字,等于说把全局作用域中的变量y给覆盖掉了。

那么如果我们在函数中确实对一个全局变量进行赋值的操作怎么办呢?

这种时候就只能用一个关键字来打破原来的规则,这个关键字就是global。


In [5]:
z = 15

def modify_global():
    global z
    z = 20
    print(z)
modify_global()
print(z)


20
20

“global z”这条语句告诉编译器,不要在局部变量中找变量z了,我要的是一个全局变量,你直接在全局作用域中,把那个名字给我找出来就好了,别做画蛇添足的事情,这个时候在modify_global函数里面使用变量z,就是直接使用的全局变量z了。

所以在函数中对z进行赋值的话,全局变量z也会收到影响。


In [39]:
def modify_define_global():
    global var
    var = 34
    print(var)
modify_define_global()
print(var)


34
34

在使用了global关键字之后,解释器默认的从全局作用域中去查找变量var,如果没有找到,解释器会为var变量分配一段内存,并在全局作用域中给这段内存添加一个名字var。

闭包作用域和局部作用域

前面说了,在局部作用域嵌套了一个局部作用域的时候,外面那层局部作用域的名字被叫做闭包作用域。


In [10]:
def outer():
    x, y = 1, 2
    def inner():
        x, y = 3, 4
        print(x, y)
    inner()
    print(x, y)
outer()


3 4
1 2

这里举了一个函数嵌套的例子,

首先有一个问题,函数在python中是一等对象,也就是说函数和普通对象的作用域是一样的,inner是定义在outer的局部作用域中的。 所以无论如何,在全局作用域中都不可以通过inner这个名字来访问到这个函数的。但是既然是一等对象,就可以用引用来访问到它,下面是绕过inner函数无法被全局作用域访问到的一种方法:


In [15]:
def out_1(x, y):
    def inner_1():
        print(x, y)
        print('hello world')
    return inner_1

In [16]:
f = out_1(1, 2)
f()


1 2
hello world

让外层函数把内层函数当作一个对象返回,在全局作用域中,用一个引用指向这个对象,那么这个对象在全局作用域也就有了自己的名字,用这个新名字就可以调用这个内层函数了。

这里还有一个非常厉害的特性,注意到上面这段代码中,在out_1函数调用完成之后,out_1的局部变量按理说就应该死掉了,但是由于inner_1函数中引用到了两个变量,而inner_1又被全局变量引用了,所以out_1函数中的局部变量的生命周期就变的和inner_1的局部变量一样长了,相当于是inner_1复制了一份out_1的作用域。


In [17]:
f2 = out_1(3, 4)
f2()


3 4
hello world

既然函数是对象,那么不同函数就应该占有不同的内存空间,在这里第二次执行了out_1函数,这个函数又定义了一个新的函数对象,并将其返回给全局作用域。 这个时候f函数和f2函数复制的out_1的作用域就变成了完全不同的两套作用域。所以打印出来的东西并不相同

这种函数嵌套的方式就叫做闭包,用法很灵活,可以举个很简单的例子:


In [19]:
def pow_n(n):
    def power(num):
        return num ** n
    return power

square = pow_n(2)
cube = pow_n(3)
print(square(5))
print(cube(5))


25
125

使用闭包可以很容易的通过外部参数定制一个新的函数,而不是说我需要一个平方的函数要实现一次,需要一个立方的函数又需要重新的实现一次。

闭包的用法后面再说,现在回到作用域。 之前全局作用域和赋值语句的冲突使用global关键字来解决的,那闭包作用域和赋值语句的冲突怎么解决呢? 如果继续使用global的话,python解释器仍然回去全局作用域中找名字。而闭包作用域并不会收到影响。


In [42]:
def out2():
    x, y = 1, 2
    def inner():
        global x, y
        x, y = 3, 4
        print(x, y)
    inner()
    print(x, y)

out2()
print(x, y)


3 4
1 2
3 4

闭包作用域和赋值语句的冲突并没有被解决。 所以,在python3中引进了一个新的关键字nonlocal,这个关键字告诉解释器,不要在这一层函数的局部作用域中去找这个名字,我要在外面一层函数的作用域中去找这个名字。


In [46]:
def out3():
    i, j = 1, 2
    def inner():
        nonlocal i, j
        i, j = 3, 4
    inner()
    print(i, j)
out3()


3 4

如果是多层嵌套怎么办?


In [48]:
def out4():
    i, j = 1, 2
    def middle():
        i, j = 3, 4
        def inner():
            nonlocal i, j
            i, j = 5, 6
        inner()
        print(i, j)
    middle()
    print(i, j)
    
out4()


5 6
1 2

可以发现nonlocal只会让解释器在最近的外层函数作用域中去查找相关的变量名,并不会影响到更外层的作用域。

那nonlocal可不可以影响到全局作用域呢?


In [50]:
abc = 123
def modify_nonlocal():
    nonlocal abc
    abc = 456

modify_nonlocal()
print(abc)


  File "<ipython-input-50-0ee48928fc91>", line 3
    nonlocal abc
    ^
SyntaxError: no binding for nonlocal 'abc' found

很明显,答案是不行,如果没有外层作用域,使用nonlocal关键字是会报错抛异常的。 所以即使在python3中,global关键字也是有它的用处的。

还要注意的是,在python2中,并没有nonlocal的关键字,如果需要对上层的值进行修改,就需要使用dict之类的对象绕过赋值语句的限制了。