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: .wrapper at 0x10a921950>

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 at 0x10a921950> 这个之下维护的,当下次执行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 所有

本文链接: https://www.heroyf.club/2019/11/08/python_closure/