python的闭包为何能保存状态?
前言
以前学习python的时候,一直看到文档上说闭包可以保存上一次执行的状态,那么为何能保存呢?
今天通过 debug 的方式,来一探究竟
代码
这是还是用之前写过的LRU缓存算法,来提现这个概念
def cache(func): data = {} def wrapper(n): if n in data: print(data) return data[n] else: res = func(n) data[n] = res return res return wrapper # fib = cache(fib) @cache def fib(n): if n <= 2: return 1 else: return fib(n - 1) + fib(n - 2) fib(10)
当我们运行这端代码的时候,得到结果
{2: 1, 1: 1, 3: 2} {2: 1, 1: 1, 3: 2, 4: 3} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21, 9: 34}
当我们再次运行这段代码,会发现结果是
{2: 1, 1: 1, 3: 2} {2: 1, 1: 1, 3: 2, 4: 3} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21, 9: 34} {2: 1, 1: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21, 9: 34, 10: 55}
第二次运行是直接命中第一次的缓存的,那么为什么呢?
闭包是怎么定义的
这里的fib如果不加语法糖的形式来写的话,其实是fib=cache(fib)
也就是这里左边的fib其实是cache返回的wrapper,
对代码加以修改
def cache(func): data = {} def wrapper(n): if n in data: print(data) return data[n] else: res = func(n) data[n] = res return res return wrapper # fib = cache(fib) @cache def fib(n): if n <= 2: return 1 else: return fib(n - 1) + fib(n - 2) def fib2(n): if n <= 2: return 1 else: return fib2(n - 1) + fib2(n - 2) fib(10) print("fib:", fib) fib(10) print("fib2:", fib2)
fib:.wrapper at 0x10a921950> fib2:
我们可以看到经过装饰器加持的函数和普通的函数是不一样的。fib其实就是wrapper
那么正常如果一个局部变量只存在于函数中,一旦这个函数执行完,这个变量便会被gc所回收,那么这里的data如何保存到下一次的状态呢?
引用qsr大佬的研究
对于cpython来说 1. python是解释型语言 2. python是没有所谓的堆空间、栈空间的 3. python的函数调用栈是由python解释器模拟的stack(栈)数据结构
也就是python是一个stack-based machine,本身虚拟机就实现了栈,有些可能会问,这里的cache里面的data不就相当于是个局部变量吗,cache执行完data难道不会被GC吗?感觉这个说法也很有道理
那么我们再来看这个fib是什么
fib:
cache是个全局函数,位于全部作用域上,而fib同样也是全局作用域上的函数,经过fib=cache(fib)这一处理后的fib相当于是由fib来进行维护的cache中的wrapper函数
通俗来说fib是一个函数对象,秉承着python一切皆对象的思想,fib指向的是cache中的wrapper,地址是0x10a921950。是不是有那么点意思?因为wrapper被fib所引用,导致wrapper不能被GC!也就是说,执行完wrapper不能被回收,又因为data在wrapper里面进行了修改,自然data的引用次数也不为0,也同样不能被GC!
所以对于整个闭包来说,data和wrapper都是位于全局作用域上的,只要整个代码段不结束,它们就一直存在于module之中,但是我们并不能通过print(data)这样的方式看到data,因为data是位于 fib:
这个之下维护的,当下次执行wrapper的时候,data就会被弹出使用。
是不是就清楚很多了?
总结
总结下来能保存状态的原因就是,fib其实指向了cache中的wrapper,wrapper中使用了data,又因为fib是全局函数,直到代码段结束都是不能被GC的,所以导致了data也不能被GC,因为它被fib所引用
那么如果我改造一下代码
def cache(func): data = {} def wrapper(n): if n in data: print(data) return data[n] else: res = func(n) data[n] = res return res return wrapper # fib = cache(fib) @cache def fib(n): if n <= 2: return 1 else: return fib(n - 1) + fib(n - 2) @cache def fib2(n): if n <= 2: return 1 else: return fib2(n - 1) + fib2(n - 2) fib(10) fib2(10)
再次打印结果,会得知,fib2其实是拿不到fib的缓存的,因为fib,fib2是两个函数对象,指向的wrapper地址是不同的,自然里面的data不共享。
fib:.wrapper at 0x10acf99e0> fib2: .wrapper at 0x10acf9b00>
从debug中我们可以看到fib和fib2都是位于全局的module之中,data就分别由fib和fib2进行维护保持。
版权声明:本文为原创文章,版权归heroyf 所有 。