《老鸟python 系列》视频上线了,全网稀缺资源,涵盖python人工智能教程,爬虫教程,web教程,数据分析教程以及界面库和服务器教程,以及各个方向的主流实用项目,手把手带你从零开始进阶高手之路!点击 链接 查看详情




闭包

阅读:23558488    分享到

闭包是函数式编程的一个重要的语法结构,简单来说闭包就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。实际上闭包可以看做一种更加广义的函数概念。因为其已经不再是传统意义上定义的函数。

闭包并不只是一个 python 中的概念,在函数式编程语言中应用较为广泛。理解 python 中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想。

闭包介绍

不同编程语言实现闭包的方式是不同的,python 中闭包从表现形式上看,如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。

def outer(x):
    def inner(y):
        return x + y
    return inner

如上代码所示,inner(y) 就是这个内部函数,对在外部作用域(但不是在全局作用域)的变量进行引用:x 就是被引用的变量,x 在外部作用域 outer 里面,但不在全局作用域里,则这个内部函数 inner 就是一个闭包。

在函数 outer 中定义了一个 inner 函数,inner 函数访问外部函数 outer 的(参数)变量,并且把 inner 函数作为返回值返回给 outer 函数。当我们调用函数的时候,必须调用外部函数,因为内部函数对我们来说是不可见的。

a = outer(150)
print(a(100))

上面的代码中,语句 a=outer(150),outer 返回一个函数(inner)赋值给 a,此时 a 就是函数 inner。然后语句 print(a(100)) 就是调用 inner 函数并传入一个参数 100,此时 a(100) 的值为 250。

一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的局部变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

闭包的注意事项

闭包无法修改外部函数的局部变量。

def outer():
    x = 200
    def inner():
        x = 250
        print("inner x:", x)

    print("outer x:", x)  # 调用 inner 函数之前
    inner()
    print("outer x:", x)  # 调用 inner 函数之后

outer()

运行结果:

outer x: 200
inner x: 250
outer x: 200

从结果上,我们可以得知 x 的值并没有被内部函数修改,在调用 inner 函数前后 x 的值没有发生变化。

闭包无法直接访问外部函数的局部变量。

def outer():
    x = 250
    def inner():  # 上面一行的 x 相对 inner 函数来说是函数外的局部变量(非全局变量)
        x += x
        return x
    return inner

outer()()

运行结果报异常:

UnboundLocalError: local variable 'x' referenced before assignment

但是我们可以间接地通过容器类型来解决,因为容器类型不是存放在栈空间的,inner 函数可以访问到。

def outer():
    x = [250]
    def inner():
        x[0] += x[0]
        return x[0]
    return inner

print(outer()())  # 500

同样,我们也可以通过 nonlocal 关键字来解决,该语句显式的指定 x 不是闭包的局部变量。

def outer():
    x = 250
    def inner():
        nonlocal x  # 把 x 声明为非局部变量
        x += x
        return x
    return inner

print(outer()())

循环中不包含域的概念。

还有一个容易产生错误的事例也经常被人在介绍 python 闭包时提起,我一直都没觉得这个错误和闭包有什么太大的关系,但是它倒是的确是在 python 函数式编程是容易犯的一个错误,我在这里也不妨介绍一下。

for i in range(10):
    pass

print(i)  # 9

在程序里面经常会出现这类的循环语句,python 的问题就在于,当循环结束以后,循环体中的临时变量 i 不会销毁,而是继续存在于执行环境中,所以在循环外部 i 的结果为 9。还有一个 python 的现象是,python 的函数只有在执行时,才会去找函数体里的变量的值。

flist = []
for i in range(3):
    def foo(x):
        print(x + i)
    flist.append(foo)

for f in flist:
    f(2)

可能有些人认为这段代码的执行结果应该是 2,3,4。但是实际的结果是 4,4,4。loop 在 python 中是没有域的概念的,flist 在向列表中添加 func 的时候,并没有保存 i 的值,而是当执行 f(2)的时候才去取,这时候循环已经结束,i 的值是 2,所以结果都是 4。

解决以上问题,改写一下函数的定义就可以了。

flist = []
for i in range(3):
    def foo(x, y=i):
        print(x + y)
    flist.append(foo)

for f in flist:
    f(2)

闭包的作用

闭包可以保存当前的运行环境,以一个类似棋盘游戏的例子来说明。假设棋盘大小为 50*50,左上角为坐标系原点(0,0),我需要一个函数,接收 2 个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。这里需要说明的是,每次运动的起点都是上次运动结束的终点。

origin = [0, 0]
legal_x = [0, 50]
legal_y = [0, 50]

def create(pos=origin):
    def player(direction, step):
        '''
        这里应该首先判断参数 direction,step 的合法性
        比如 direction不能斜着走,step不能为负等
        然后还要对新生成的 x,y 坐标的合法性进行判断处理
        这里主要是想介绍闭包,算法我就不详细写了。
        '''

        new_x = pos[0] + direction[0]*step
        new_y = pos[1] + direction[1]*step
        pos[0] = new_x
        pos[1] = new_y

        # 注意!此处不能写成 pos = [new_x, new_y],因为参数变量不能被修改,而 pos[] 是容器类的解决方法
        return pos
    return player

player = create()  # 创建棋子 player,起点为原点
print(player([1, 0], 10))   # 向x轴正方向移动10步
print(player([0, 1], 20))   # 向y轴正方向移动20步
print(player([-1, 0], 10))  # 向x轴负方向移动10步

运行结果:

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

也就是我们先沿X轴前进了 10,然后沿 Y 轴前进了 20,然后反方向沿X轴退了 10,坐标分别为:[10,0], [10, 20], [0, 20]。

当然,闭包在爬虫以及 web 应用中都有很广泛的应用,并且闭包也是装饰器的基础,这些内容笔者会在后续的文章中分别介绍,这里就不多谈了。理解了本文中的概念,你应该知道的关于闭包的知识也差不多了,请在自己的编程中尽情使用吧。

本节重要知识点

会使用闭包。

注意闭包使用的注意事项。

把闭包适当的应用到编程中。

作业

著名公司(某度)笔试题:写出下面程序运行结果。

def fun():
    temp=[lambda x:x*i for i in range(4)]
    return temp
for every in fun():
    print(every(2))

如果以上内容对您有帮助,请老板用微信扫一下赞赏码,赞赏后加微信号 birdpython 领取免费视频。


登录后评论