Python闭包与nonlocal

在廖雪峰的官网上看到一个很有意思题目。关于闭包的,有兴趣的朋友可以看一下这里, 做一下这个题目,当然需要一点闭包的知识。下面我简述一下:

利用闭包返回一个计数器函数,每次调用它返回递增整数。

# 修改下面这个函数
def createCounter():
   def counter():
       pass
   return counter
# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('测试通过!')
else:
    print('测试失败!')

方法一

说实话这题对我来说还是有点难度的,但我尝试了几次之后也找到一个比较track的方法。一开始我是这么写的。

def createCounter():
    i = 0
    def counter(i=i):
        i = i+1
        return i
    return counter
# 执行结果是: 1 1 1 1 1

这样当然是错的, 因为整数 是 不可变对象,当你作为参数传进去时都会创建一个新的内存空间,这里边其实还有很多学问,不是很了解的可以看一下stackoverflow上的这个回答。虽然失败了,但也让我想到一个track的方法,就是把i换成可变对象

def createCounter():
    i = [0]
    def counter():
        i[0] = i[0]+1
        return i[0]
    return counter
# 执行结果是: 1 2 3 4 5

OK, 这样就没有问题了。但这并不是一个好的解决方法, 利用可变对象的这个特性有可能会引起变量作用域混乱的。于是我又想到了另一种解决。

方法二

另一种方法就是使用generator,在createCounter函数下创建一个从1开始的整数generator, 然后在cuonter函数中调用。由于generator保存的是算法,当调用next函数时就可以计算出下一个的值,直到没有元素报错。当然这里不用担心,generator可以创建无限集合。

def createCounter():
    def inter():
        n = 1
        while True:
            yield n
            n = n+1 
    f = inter()
    def counter():
        return next(f)
    return counter

上面的代码中,inter()就是一个包含从1开始的所有整数的generator。然后在counter里边调用。每次计算下一个的值。这样就可以实现计数的功能。说到generator,stackoverflow上有一个回答值得一读,即使你已经掌握这个也可以读一下,这个回答应该还是python问答当中排名第一的。链接在这里

方法三

emmmm,想到这两种方法已经是极限了,于是我往评论区翻了翻,看一下大佬们有什么做法。然后就看到一个我没见过的关键字...其中有一个大佬是这么做

def creat_counter():
    i=0
    def counter():
        nonlocal i
        i=i+1
        return i
    return counter

学了python这么久,第一次看到nonlocal这个关键字,果然我还是太菜了。。。
不过从语句上看nonlocal的作用应该是把i变成全局变量,这样每次修改都可以生效,跟global关键字有点像。既然找到一个知识盲点,那就将它彻底解决吧。

nonlocal 与 global

说了这么多,是时候回到主题了,nonlocal关键字到底是什么?在什么情况下用呢?简单来说,nonlocal关键字是用来改变变量的作用域的,直接解释不太好懂。先来看两个例子吧。

def outside():
    msg = "Outside!"
    def inside():
        msg = "Inside!"
        print(msg)
    inside()
    print(msg)

执行结果是什么呢?

Inside!
Outside!

结果应该很好理解, 在outside函数里面定义了insede函数并且执行。当运行outside函数时,inside里面的msg变量指向了"Inside!",outside里面的msg指向了"Outside!", 也就是说这里其实有两个msg变量,并且指向了不同的值。如下图所示:


变量示意图

再来看下面这个例子:

def outside():
    msg = "Outside!"
    def inside():
        nonlocal msg
        msg = "Inside!"
        print(msg)
    inside()
    print(msg)

现在的执行结果就变成了:

Inside!
Inside!

两段代码之间的差别仅在于下面的例子多了一句 nonlocal msg。这里的nonlocal关键字起到了什么作用呢?nonlocal意思是告诉python,不要重新创建msg变量,而是使用outside中的msg变量来赋值。画个图就很好懂了。

变量示意图

在这个例子中, msg变量只被创建了一次,首先将"Outside!"赋值给msg,然后将"Inside!"赋值给了msg, 此时的msg已经指向了"Inside!"。因此执行的结果两个都是"Inside!"。

现在我们知道了,nonlocal是用来改变变量的作用域的。本例中,nonlocal将inside函数里面的msg变量的作用域变成了outside块中的区域。nonlocal跟global 这两个关键字非常像,不同之处在于nonlocal用于外部函数作用域的变量,而global用于全局范围内的变量。

这就是nonlocal关键字的作用。但是还有一点值得注意,先看下面的例子。

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

大家觉得输出是什么呢?
实际输出是这样的:

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

原因嘛,当然是因为dict是可变对象了,但由于 d["inside"] = 2 这样的语句是有点让人迷惑的,看起来很像重新赋值。实际上dict的赋值是调用了setitem方法。这样看就不会感到迷惑了。

# 下面的代码是等价的。
d["inside"] = 2
d.__setitem__("inside", 2)

关于nonlocal关键字,应该讲清楚了吧。至于python的闭包,其实还是挺复杂的,上面的只是几个例子,想要更加深入的学习可以上stackoverflow上看看大佬们的回答。很有学习的价值。

写文不易, 还请大家多多支持!

参考资料:

  1. https://www.smallsurething.com/a-quick-guide-to-nonlocal-in-python-3/
  2. https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317799226173f45ce40636141b6abc8424e12b5fb27000
  3. https://taizilongxu.gitbooks.io/stackoverflow-about-python/content/1/README.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章