跳过正文
  1. 技术杂项/

Python 闭包误区

·173 字·1 分钟

闭包其实就是一个作用域的概念,即在内部函数中,允许访问外部变量。

但是,Pyhton 中闭包的相关性质,和 JS 相比较有很多的区别。(Python 的作用域设计本就与其它语言有不小区别。)

总的来说呢就是,在闭包函数内,可以使用外部变量的值,但不能修改外部变量。

比如以下代码的运行是符合预期的:

def func():
    a = 1

    def innerFunc():
        print(a)

    innerFunc()

func()
# 输出: 1

我们在 innerFunc 中只是打印了 a 的值,没有对 a 进行修改。

再看如下代码:

def func():
    a = 1

    def innerFunc():
        a = 2
        print(a)

    innerFunc()
    print(a)

func()
# 输出:
# 2
# 1

在此例中,我们发现闭包函数中打印出 a 为 2, 而外部函数中 a 为 1. 这就是前面所说的,闭包函数只能使用外部变量的值,却不能对外部变量进行修改。实际上,当我们在内部函数中声明 a = 2 时,已经创建了一个新的局部变量 a, 这个局部变量会在闭包函数中屏蔽外部的变量 a,因而在闭包函数中所做的任何操作都是针对这个局部变量的,对外部变量 a 不会产生任何影响。

以上的代码都没有报错,我们来看下面这段代码:

def func():
    a = 1

    def innerFunc():
        a += 1
        print(a)

    innerFunc()
    print(a

func()
# 输出
# 错误:UnboundLocalError: local variable 'a' referenced before assignment

在此例中,我们将闭包函数中的 a = 2,修改为了 a += 1,代码报错了。延续前面所阐述的概念,我们要对一个名为 a 的变量进行自加操作,即修改和赋值,而当前命名空间内,并不存在一个已被初始化的可修改的 a 变量(闭包函数无法修改外部变量,执行此语句时,外部的 a 变量实际已经被屏蔽了)。对一个不存在变量进行自加操作,所报出来的错误自然就是:变量在赋值前被引用。

我们直到,Python 中的变量可分为值变量和引用变量(实际从本质上来说,Python 中的所有变量都是引用变量,详细看此文章),由于引用变量的存在,某些场景下,我们会产生闭包函数可以修改外部变量的错觉。

看如下代码:

def func():
    a = [1]

    def innerFunc():
        a[0] += 1
        print(a[0])

    innerFunc()
    print(a[0])

func()
# 输出:
# 2
# 2

这段代码的执行结果不免使人疑惑,从这个结果来看,我们不是成功地在闭包函数内修改了 a[0] 的值吗?是的,我们确实成功修改了,但是这和前面所述的闭包函数不能修改外部变量冲突吗?不冲突。我们知道,这段代码与上一段的区别是,a 从一个整形变量变成了列表,而列表是一个引用变量。也就是说 a 变量存储的是列表的首地址。我们在闭包函数中修改了 a[0] 的值后,a 变量变化了吗,并没有,a 变量中存储的仍然是列表的首地址。也就是说,我们并没有在闭包函数中修改所谓的外部变量 a,我们只是根据外部变量 a 找到了列表的首地址,然后对列表进行了修改(列表是存储在堆中的,通过地址访问,就不存在什么作用域的问题了)。所以这段代码,乍一看好像闭包函数修改了外部变量,其实没有这回事儿,只是在闭包函数内操作了一下堆中的数据罢了。

搞明白了其背后的根本原因,下面的代码,自然也迷糊不了你:

def func():
    a = [1]

    def innerFunc():
        a = [2]
        print(a[0])

    innerFunc()
    print(a[0])

func()
# 输出:
# 2
# 1

闭包函数中的 a = [2] 又是对 a 变量的一个赋值的操作,遵从前面所属的原则,此时闭包函数内创建了一个新的局部变量 a(这次是一个引用变量),存储了一个新的列表的首地址。无论是针对它,还是针对其所指向的列表的操作,自然也就不会影响外部变量 a 及其所指向的列表了。