【Python语法】循序渐进理解闭包

1. 闭包初接触

在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。

1.1 闭包基本语法结构:

# 外部函数返回内部函数
def outside(attr1):
    # 内部函数使用了外部函数的变量
    def inside(attr2):
        return attr1 + attr2
	# 外部函数返回内部函数
    return inside


# 因为outside()返回的是函数,所以a也是一个函数
a = outside(1)
print(a)
# 因此可以对函数传入参数,按照函数的执行顺序,返回
print(a(2))

结果:

<function outside..inside at 0x00000229B7D93828>
3

通过阅读代码注释,可以理解最终的运行结果,但是要深入理解闭包,还需要往下看

1.2 明确变量作用域:

闭包中可能会涉及到内部函数访问外部函数或者全局的变量,复习下Pyhon变量作用域,为学习闭包语法扫清障碍:

  1. 内部函数无法修改外部函数的值:因为内部函数的变量作用域不在外部
def outter():
    x = 1
    def inner():
        x = 2
        print('x in inner: %d' % x)
    print('x in outter before call inner: %d' % x)
    inner()
    print('x in outter after call inner: %d' % x)
o = outter()

结果

x in outter before call inner: 1
x in inner: 2
x in outter after call inner: 1

  1. 闭包中的访问外部变量:nonlocal或者global
    nonlocal 语句会去搜寻本地变量与全局变量之间的变量,其会优先寻找层级关系与闭包作用域最近的外部变量。
attr1 = 1
attr2 = 11
def outfunc():
    attr1 = 2
    attr2 = 22
    def inFunc():
        nonlocal attr1
        global attr2
        attr1 += 10
        attr2 += 100
        print('inner attr1: %d' %  attr1)
        print('inner attr2: %d' %  attr2)
    print('outter attr1 before call inFunc: %d' % attr1)
    print('outter attr2 before call inFunc: %d' % attr2)
    inFunc()
    print('outter attr1 after call outFunc: %d' % attr1)
    print('outter attr2 after call outFunc: %d' % attr2)

test = outfunc()

结果:

outter attr1 before call inFunc: 2
outter attr2 before call inFunc: 22
inner attr1: 12
inner attr2: 111
outter attr1 after call outFunc: 12
outter attr2 after call outFunc: 22

2. 从for循环开始

2.1 python for循环特性:没有域的概念

先阅读一段代码

flist = []

for i in range(3):
    def innerfunc(x):
        return x*i
    flist.append(innerfunc)


for f in flist:
    print(f(2))

让我们猜猜最终的结果,应该是2乘以列表[0,1,2]的每个元素,得到[0,2,4]
运行结果:

4 4 4

加断点分析:

  1. 第一个for循环的元素结果是一个列表,列表元素都是函数,生成列表后,列表中的参数都不赋值,也不进行计算

flist = [<function innerfunc at 0x0000023575C390D8>,
<function innerfunc at 0x0000023575C39168>,
<function innerfunc at 0x0000023575C2EDC8>]

可以认为是 [x * i, x * i, x * i],此时作用域中i为2, 即列表为[x* 2, x * 2, x * 2]

  1. 第二个for循环遍历flist,传入参数x的值,此时列表结果即为[2 * 2, 2 * 2, 2 * 2]

2.2 修改代码,让返回的flist具有递增相乘的结果:

flist = []

for i in range(3):
    def innerfunc(x):
        return x*i
    flist.append(innerfunc)
    # 在第一次循环的时候,就对flist中的函数赋值,即避免多次遍历flist
    print(flist[i](2))

执行结果:

0
2
4

2.3 使用闭包

闭包的外部函数可以保证每次给函数列表flist新增元素时,i值已经固定

flist = []
for i in range(3):
    def makerfun(i):
        def fun(x):
            return x*i
        return fun
    flist.append(makerfun(i))

for f in flist:
    print(f(2))

加断点分析:

  1. flist = [<function makerfun..fun at 0x000002DB3E22A168>, <function makerfun..fun at 0x000002DB3E21FDC8>, <function makerfun..fun at 0x000002DB3E21FCA8>]

flist依旧是一个函数列表:[makerfun(0), makerfun(1),makerfun(2)]
又因为列表元素是一个闭包,即[x * 0, x * 1, x * 2]
在第一个for循环结束后,i的值已经固定下来了

  1. 第二个for循环flist,传入参数x的值,此时列表结果即为[2 * 0, 2 * 1, 2 * 2]
    执行结果:

0
2
4

体会过上面的例子后,就很好理解闭包的作用:保存当前的运行环境

拿上面的例子来讲,x始终是我们计算时要传入的变量,i看做每次for循环运行的环境标识。
闭包实现了让外部函数保存了每次for循环的环境标识,当我们回头再为flist的每个元素函数传入变量x时,环境标识被闭包固化。

3. 玩棋盘游戏

模拟棋盘游戏,使用闭包,让外部函数实时保存出发的坐标,内部函数完成棋子按照指定方向和步长移动的任务

origin = [0, 0]


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


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

看到这段代码是不是对闭包的语法有了更深的理解,注意对照这句话体会:

闭包持有外部函数的变量,这个变量叫做自由变量,当外部函数的声明周期结束后,自由变量依然存在,因为它被闭包引用了,所以不会被回收

4. 闭包特性的另一种实现

闭包这个特性可以用类实现,把类变量看做是自由变量,类的实例持有类变量
但是使用闭包会比使用类占用更少的资源,自由变量占用内存的时间更短

class Animal():
    def __init__(self,animal):
        self.animal = animal
    def sound(self,voice):
        print(self.animal, ':', voice)

dog = Animal('dog')
dog.sound('wangwang')
dog.sound('wowo')
def voice(animal):
    def sound(voc):
        print(animal,':', voc)
    return sound

dog = voice('dog')
dog('wangwang')
dog('wowo')

可以看到输出结果是完全一样的,但显然类的实现相对繁琐,且这里只是想输出一下动物的叫声,定义一个 Animal 类未免小题大做,而且 voice 函数在执行完毕后,其作用域就已经释放,但 Animal 类及其实例 dog 的相应属性却一直贮存在内存中

5. 爬虫中的闭包实践

需求描述:通过向指定url发送GET请求,获取网页中的信息

实现方式1:直接通过request.get()方法获取到网站内容

实现方式2:延迟调用,通过闭包,实现针对不同网站获取信息的函数,在需要爬取的时候再调用

import requests
def get_html(url):
    response = requests.get(url)
    print(response.text)
    
get_html('https://www.baidu.com')

# 重新实现上面的方法,通过闭包,实现延迟调用
def outer_get(url):
    def get_html(url):
        response = requests.get(url)
        print(response.text)
    return get_html

show_baidu = outer_get('https://www.baidu.com')
show_bilibli = outer_get('htpps://www.bilibili.com')
# 延迟调用
show_baidu()
show_bilibili()

6. 闭包实现流水线作业

(待补充)

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页