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函数里面定义了inside函数并且执行。当运行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上看看大佬们的回答。很有学习的价值。
写文不易, 还请大家多多支持!
参考资料: