在进入正题之前先简单说明一下:

  • 局部变量:函数或者方法里面定义的变量;
  • 全局变量:本文指module级别的变量(一个python文件就是一个module)。

关于Python的作用域这里推荐一篇文章:A Beginner's Guide to Python's Namespaces, Scope Resolution, and the LEGB Rule,有兴趣的可以看一下。

global

先看个例子:

a = 1

def func1():
    print("func1:", a)

def func2():
    a = 10
    print("func2:", a)

def func3():
    print("func3:", a)
    a = 100
    print("func3:", a)


if __name__ == "__main__":
    print(a)

    func1()

    func2()
    print(a)

    func3()

输出结果如下:

1
func1: 1
func2: 10
1
Traceback (most recent call last):
  File "/Users/allan/Desktop/test/test/test.py", line 24, in <module>
    func3()
  File "/Users/allan/Desktop/test/test/test.py", line 11, in func3
    print("func3:", a)
UnboundLocalError: local variable 'a' referenced before assignment

func1func2的输出应该没有什么疑问,就是局部变量和全局变量作用域的问题:模块内都可以访问到全局变量;函数内局部变量与全局变量重名,会将全局变量隐藏掉(这种最好的理解方式就是把局部变量换个名字)。这里不再赘述。func3中,因为定义了和全局变量同名的a,所以,全局变量在整个方法中都被隐藏掉了,这个与局部变量a定义的位置没有关系。所以,在局部变量a定义之前使用a就会报异常。

这里需要说明的一个关键点是:函数或方法中的任何地方,当给某个变量赋值的时候(特别要注意这个场景限定,后面会有一个例子),Python会将该变量当成一个局部变量,除非显式的使用global关键字声明该变量为全局变量。当然,赋值是写的过程,如果是读的话,会先查有没有这个名字的局部变量,如果没有,再查有没有这个名字的全局变量。

所以说,一般global的使用场景就是当我们想在函数里面修改全局变量的值。看下面代码:

a = 1

def func4():
    global a
    a = 100
    print("func4:", a)

if __name__ == "__main__":
    print(a)
    func4()
    print(a)

输出如下:

1
func4: 100
100

借助于global关键字,我们成功的在函数func4里面修改了全局变量a的值。func4和之前的func2的唯一区别就在于没有使用global关键字。

所以,这样来看,其实global的使用还是挺简单的。下面看另外一个和global作用非常像的关键字nonlocal

nonlocal

nonlocal关键字是Python3引入的(见PEP 3104 - Access to Names in Outer Scopes, The specification for the nonlocal statement.),PEP里面对该关键字的作用已经有了大概的说明了:访问作用域之外的(对象)名字。

还是先看个例子:

def outside():
    msg = 'outside'
    def inside():
        msg = 'inside'
        print(msg)
    inside()
    print(msg)

if __name__ == "__main__":
    outside()

这个例子和之前的例子实质一样,inside里面的msg变量其实是inside函数创建的一个局部变量而已,只不过和外层的msg同名了而已,但实质还是两个完全不同的变量。所以输出也并不意外:

inside
outside

现在问题来了,如果我想在inside里面给外层的msg重新赋值(即修改外层的msg)怎么办?使用刚才介绍的global关键字行不行?试一下便知:

def outside():
    msg = 'outside'
    def inside():
        global msg
        msg = 'inside'
        print(msg)
    inside()
    print(msg)

if __name__ == "__main__":
    outside()

输出结果:

inside
outside

Oh, holy shit,没有成功。其实也正常,因为外层的msg并不是一个全局变量,它的作用域并不是module级别的,而只是outside函数范围内的而已。现在能实现我们上面那个需求的方式就是用nonlocal代替上面代码中的global

def outside():
    msg = 'outside'
    def inside():
        nonlocal msg
        msg = 'inside'
        print(msg)
    inside()
    print(msg)

if __name__ == "__main__":
    outside()

输出如下:

inside
inside

所以,nonlocal这个关键字主要就是在闭包函数(enclosing function or closures)中使用,可以修改外层函数里面的对象。

综合来看nonlocalglobal的确是非常像的,不同之处在于前者用于修改函数作用域内的局部变量,而后者用于修改module级别的全局变量。

提提神

文章最后,再来一个提神的,看下面代码输出什么:

def outside():
    msg = {"outside": 1}
    def inside():
        msg['inside'] = 2
        print(msg)
    inside()
    print(msg)

if __name__ == "__main__":
    outside()

输出结果如下:

{'outside': 1, 'inside': 2}
{'outside': 1, 'inside': 2}

Oh, holy shit,有没有意外?这里没有使用nonlocal修改msg,但里面的inside函数还是修改了外边的msg。不是说不行吗?

nonlocal:这个锅我不背。

其实这个的确和nonlocal没有什么关系,文章前面已经提醒过了,只有在给变量赋值这个场景下,且没有使用globalnonlocal,Python才去创建本函数的局部变量,而不是访问全局或外层函数的变量。而上面的msg['inside'] = 2其实不是个变量赋值语句。

What?没错,字典的插入的确不是变量赋值,而是方法调用(调用的是__setitem__方法)。所以,msg['inside'] = 2的底层其实是执行了如下语句:

msg.__setitem__("inside", 2)

有没有很提神?(〃'▽'〃)

References