Python-闭包函数及深入

发布时间 2023-08-24 10:46:47作者: 王寄鱼

一、闭包函数

(一)什么是闭包函数

python的装饰器是依靠闭包函数实现的。

以下为维基百科的定义:

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

通俗来讲闭包函数的定义是:一个函数的返回值是其内部定义的函数名(对于操作系统来说是一串内存地址),且内部函数包含对外部作用域的引用

比如一下函数,就是一个最简单的闭包函数

# func引用了func1的变量obj
def func1(obj):
    def func():
        print(obj)

(二)为什么需要闭包

在某些情况下,我们需要在函数外部,拿到其内部的变量。因为python的作用域搜索顺序为链式作用域结构:子对象会一级一级地向上寻找所有父对象的变量,通常情况下,没办法做到这一点。

比如下面这段代码

def func():
    n = 99
    

print(n)

==========================
Traceback (most recent call last):
  File "test.py", line 5, in <module>
    print(n)
NameError: name 'n' is not defined

我们利用python的作用域搜索来做一下修改,变成以下函数,成功在函数外部,拿到了n的值

def func():
    n = 99

    def func1():
        print(n)

    return func1


res = func()
res()

================
99

(三)闭包函数作用

根据维基百科的定义,我们可以总结闭包函数的两个作用

作用1:闭包能将函数外部与其内部连通起来,创建关联关系。
作用2:将外层函数的变量持久地保存在内存中。(这个大部分人都没有讲解到,下文会详讲)

1、读取函数内部的变量

以下引用一位大佬的描述,很好的描述了对闭包的理解与作用。

闭包存在的意义就是它夹带了外部变量(私货),如果它不夹带私货,它和普通的函数就没有任何区别。同一个的函数夹带了不同的私货,就实现了不同的功能。
其实你也可以这么理解,闭包和面向接口编程的概念很像,可以把闭包理解成轻量级的接口封装。
----@Wayne

def tag(tag_name):
    def add_tag(content):
        return f"{tag_name},{content}"

    return add_tag


content = 'Hello'

add_tag = tag('a')
print(add_tag(content))

add_tag = tag('b')
print(add_tag(content))


==============================
a,Hello
b,Hello

在这个例子里,我们想要一个给contenttag的功能,但是具体的tag_name是什么样子的要根据实际需求来定,对外部调用的接口已经确定,就是add_tag(content)。如果按照面向接口方式实现,我们会先把add_tag写成接口,指定其参数和返回类型,然后分别去实现a和b的add_tag
但是在闭包的概念中,add_tag就是一个函数,它需要tag_namecontent两个参数,只不过tag_name这个参数是打包带走的。所以一开始时就可以告诉我怎么打包,然后带走就行。
----@Wayne

2、缓存变量

闭包还能将变量缓存,使其不被gc机制回收,当然这一特点如果运行的不好也会导致内存泄露。

看一个阮一峰的例子

def create(pos=[0,0]):
    
    def go(direction, step):
        new_x = pos[0]+direction[0]*step
        new_y = pos[1]+direction[1]*step
        
        pos[0] = new_x
        pos[1] = new_y
        
        return pos
    
    
    return go

player = create()
print(player([1,0],10))
print(player([0,1],20))
print(player([-1,0],10))

-------------------------------
[10,0]
[10,20]
[0,20]

在这段代码中,player实际上就是闭包go函数的一个实例对象。

它一共运行了三次,第一次是沿X轴前进了10来到[10,0],第二次是沿Y轴前进了20来到 [10, 20],,第三次是反方向沿X轴退了10来到[0, 20]。

这证明了,函数create中的局部变量pos一直保存在内存中,并没有在create调用后被自动清除。

为什么会这样呢?原因就在于create是go的父函数,而go被赋给了一个全局变量,这导致go始终在内存中,而go的存在依赖于create,因此create也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这个时候,闭包使得函数的实例对象的内部变量,变得很像一个的实例对象的属性,可以一直保存在内存中,并不断的对其进行运算。

参考文档:

1、Python闭包(Closure)详解 - 知乎 (zhihu.com)

2、[深入浅出python闭包 - 知乎 (zhihu.com)](