Python进阶 三器一闭-1.3闭包
1. 目的
想想看怎样用程序实现下面的功能呢?
- 有2个人在说话,说话的顺序可能不同
- 每次说话的时候,都要标记是谁说的话
今天我们要研究的知识点是 “闭包”,实现上述功能的方式可能有多种,但是闭包会更简单
2. 尝试解决上述问题
2.1 尝试1(最普通的方式)
def say(user_name, content):
print("(%s):%s" % (user_name, content))
user_name1 = "张三"
user_name2 = "李四"
say(user_name1, "你努力了吗?")
say(user_name2, "为啥努力!")
say(user_name1, "你确定不要努力吗?")
say(user_name2, "嗯,确定?")
say(user_name1, "那可就不要要怪别人努力了啊")
say(user_name2, "别人与我何关!")
say(user_name1, "隔壁那户人家姓xxxx")
say(user_name2, "( ⊙ o ⊙ )啊!")
运行效果如下:

小总结:
上述代码,已经实现了要求,但是不觉得麻烦吗?每次都要将用户的名字传递到say函数中,肯定有办法来解决这个问题,那你觉得是什么呢?
2.2 尝试2(面向对象)
class Person(object):
def __init__(self, name):
self.user_name = name
def say(self, content):
print("(%s):%s" % (self.user_name, content))
p1 = Person("张三")
p2 = Person("李四")
p1.say("你努力了吗?")
p2.say("为啥努力!")
p1.say("你确定不要努力吗?")
p2.say("嗯,确定?")
p1.say("那可就不要要怪别人努力了啊")
p2.say("别人与我何关!")
p1.say("隔壁那户人家姓xxxx")
p2.say("( ⊙ o ⊙ )啊!")
运行效果:

小总结:
通过面向对象的方式能够实现上述要求,但是发现使用了类以及对象,总体感觉还是较为复杂,再者说继承的object类中有很多默认的方法,既然这个程序不需要,显然会造成一定的浪费
是否有更简单的方式呢?
2.3 尝试3(闭包)
def person(name):
def say(content):
print("(%s):%s" % (name, content))
return say
p1 = person("张三")
p2 = person("李四")
p1("你努力了吗?")
p2("为啥努力!")
p1("你确定不要努力吗?")
p2("嗯,确定?")
p1("那可就不要要怪别人努力了啊")
p2("别人与我何关!")
p1("隔壁那户人家姓xxxx")
p2("( ⊙ o ⊙ )啊!")
运行效果:

稍加完善代码:
def who(name):
def do(content):
print("(%s):%s" % (name, content))
return do
zhangsan = who("张三")
lisi = who("李四")
zhangsan("你努力了吗?")
lisi("为啥努力!")
zhangsan("你确定不要努力吗?")
lisi("嗯,确定?")
zhangsan("那可就不要要怪别人努力了啊")
lisi("别人与我何关!")
zhangsan("隔壁那户人家姓xxxx")
lisi("( ⊙ o ⊙ )啊!")

估计第一次看到函数嵌套调用,你会很惊讶,不用着急,这就是我们今天的主角“闭包”
3. 闭包
3.1 引用
# 定义函数可以理解为:
# 定义了一个全局变量,其变量名字是函数的名字,即test
# 这个test变量指向了一个代码块,这个代码块是函数
# 其实就是说test保存了一个代码块的地址,即引用
def test():
print("--- in test func----")
test() # 这是调用函数
ret = test # 用另外一个变量 复制了 test这个引用,导致ret变量也指向那个 函数代码块
# 下面输出的2个地址信息是相同的
print(id(ret))
print(id(test))
# 通过引用调用函数
ret()
运行结果:
--- in test func---- 140212571149040 140212571149040 --- in test func----
3.2 什么是闭包
闭包(closure) 定义非常抽象,很难看懂
下面尝试从概念上去理解一下闭包
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。 —— 维基百科
https://zh.wikipedia.org/wiki/闭包_(计算机科学)
用比较容易懂的人话说:就是当某个函数被当成对象返回时,夹带了外部变量,就形成了一个闭包
可以这样理解,闭包就是能够读取其他函数内部变量的函数
看如下案例,便于理解什么是闭包
def make_printer(msg): # 可以认为是 外部函数
def printer(): # 可以认为是 内部函数
print(msg)
return printer # 返回的内部函数的引用
printer = make_printer('Good!')
printer()
运行效果:

4. 闭包案例
4.1 案例1(简单计算数值)
def test(number):
def test_in(number_in):
print("in test_in 函数, number_in is %d" % number_in)
return number+number_in
return test_in
# 给test函数赋值,这个20就是给参数number
ret = test(20)
# 注意这里的100其实给参数number_in
print(ret(100))
# 注意这里的200其实给参数number_in
print(ret(200))
运行结果:

4.2 案例2(计算坐标)
def line_conf(a, b):
def line(x):
return a*x + b
return line
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5))
print(line2(5))
运行效果:

这个例子中,函数line与变量a,b构成闭包。
在创建闭包的时候,我们通过line_conf的参数a,b设置了这两个变量的取值,这样就确定了函数的最终形式(y = x + 1和y = 4x + 5)。
如果需要修改这条线的信息,只需要变换参数a,b,就可以获得不同的直线表达函数。
由此,我们可以看到,闭包也具有提高代码可复用性的作用
如果没有闭包,我们需要每次创建直线函数的时候同时说明a,b,x。这样,我们就需要更多的参数传递,也减少了代码的可移植性
5. 注意点
5.1 使用闭包应注意的问题
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。因此可以手动解除对匿名函数的引用,以便释放内存。
5.2 修改外部函数中的变量
def counter(start=0):
def add_one():
nonlocal start
start += 1
return start
return add_one
c1 = counter(5) # 创建一个闭包
print(c1())
print(c1())
c2 = counter(50) # 创建另外一个闭包
print(c2())
print(c2())
print(c1())
print(c1())
print(c2())
print(c2())
运行效果:

5.3 多个闭包
如上面的代码中,调用了2次counter,也就意味着创建了2个闭包,并且每个闭包之间没有任何关系。
大家是否有种感觉,好像闭包与对象有些类似。确实是这样的,对象其实可通俗的理解为数据(属性)+功能(方法),而闭包也可以理解为数据+功能,只不过此时数据是外部函数中的那些局部变量或者形参,而功能则是内部函数。对象适合完成较为复杂的功能,而闭包则更轻量
6. 充分理解闭包
问题:初中里学过函数,例如y=kx+b
以y=kx+b为例,请计算一条线上的多个点 即 给x值 计算出y值
6.1 方式1
# 第1种 k = 1 b = 2 y = k*x+b
缺点:如果需要多次计算,那么就的写多次y = k*x+b这样的式子
6.2 方式2
# 第2种
def line_2(k, b, x):
print(k*x+b)
line_2(1, 2, 0)
line_2(1, 2, 1)
line_2(1, 2, 2)
缺点:如果想要计算多次这条线上的y值,那么每次都需要传递k,b的值,麻烦
6.3 方式3
# 第3种: 全局变量
k = 1
b = 2
def line_3(x):
print(k*x+b)
line_3(0)
line_3(1)
line_3(2)
k = 11
b = 22
line_3(0)
line_3(1)
line_3(2)
缺点:如果要计算多条线上的y值,那么需要每次对全局变量进行修改,代码会增多,麻烦
6.4 方式4
# 第4种:缺省参数
def line_4(x, k=1, b=2):
print(k*x+b)
line_4(0)
line_4(1)
line_4(2)
line_4(0, k=11, b=22)
line_4(1, k=11, b=22)
line_4(2, k=11, b=22)
优点:比全局变量的方式好在:k, b是函数line_4的一部分 而不是全局变量,因为全局变量可以任意的被其他函数所修改
缺点:如果要计算多条线上的y值,那么需要在调用的时候进行传递参数,麻烦
6.5 方式5
# 第5种:实例对象
class Line5(object):
def __init__(self, k, b):
self.k = k
self.b = b
def __call__(self, x):
print(self.k * x + self.b)
line_5_1 = Line5(1, 2)
# 对象.方法()
# 对象()
line_5_1(0)
line_5_1(1)
line_5_1(2)
line_5_2 = Line5(11, 22)
line_5_2(0)
line_5_2(1)
line_5_2(2)
缺点:为了计算多条线上的y值,所以需要保存多个k, b的值,因此用了很多个实例对象, 浪费资源
6.6 方式6
# 第6种:闭包
def line_6(k, b):
def create_y(x):
print(k*x+b)
return create_y
line_6_1 = line_6(1, 2)
line_6_1(0)
line_6_1(1)
line_6_1(2)
line_6_2 = line_6(11, 22)
line_6_2(0)
line_6_2(1)
line_6_2(2)
6.7思考
函数、匿名函数、闭包、对象 当做实参时 有什么区别?
-
匿名函数能够完成基本的简单功能,,,传递是这个函数的引用 只有功能
-
普通函数能够完成较为复杂的功能,,,传递是这个函数的引用 只有功能
-
闭包能够将较为复杂的功能,,,传递是这个闭包中的函数以及数据,因此传递是功能+数据
- 对象能够完成最为复杂的功能,,,传递是很多数据+很多功能,因此传递是功能+数据
7. 闭包应用
7.1 应用1
下面应用案例是理解闭包的经典题目,模拟了一个人站在原点,然后向X、Y轴进行移动,每次移动后及时打印当前的位置
def create():
pos = [0, 0] # 坐标系统原点
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
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步
运行效果:

7.2 应用2
有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行
例如,需要取得文件"result.txt"中含有"163.com"关键字的行,看如下代码
def make_filter(keep):
def the_filter(file_name):
file = open(file_name)
lines = file.readlines()
file.close()
filter_doc = [i for i in lines if keep in i]
return filter_doc
return the_filter
filter = make_filter("163.com")
filter_result = filter("result.txt")
运行效果:

8. 总结
- 闭包定义是在函数内再嵌套函数
- 闭包是可以访问另一个函数局部作用域中变量的函数
- 闭包可以读取另外一个函数内部的变量
- 闭包可以让参数和变量不会被垃圾回收机制回收,始终保持在内存中(而普通的函数调用结束后 会被Python解释器自动释放局部变量)

浙公网安备 33010602011771号