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解释器自动释放局部变量)