本节摘要:高阶函数之 返回函数和闭包
高阶函数
返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
例如,定义一个求和函数:
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax
如定义一个函数,在函数内部再定义一个函数,而外面的函数把内部的求和函数作为返回值。
def sum1(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用sum1()
时,返回的并不是求和结果,而是求和函数sum,而不是整数1,3,5的求和结果:
>>> f = sum1(1, 3, 5)
>>> f
<function sum at 0x10d088c80>
调用函数f时,才真正计算求和的结果:
>>> f()
9
需要注意的是,每次调用sum1()
,都会返回一个新的函数,即使传入的参数是相同的。
>>> f1 = sum1(1, 3, 5)
>>> f2 = sum1(1, 3, 5)
>>> f1 == f2
False
f1()
和f2()
的调用结果互不影响。
闭包
在上面的例子中,我们称sum1为外部函数,sum为内部函数,在函数sum1中定义了函数sum,并且,内部函数sum可以引用外部函数sum1的参数和局部变量,当外部函数sum1返回内部函数sum时,相关参数和局部变量都保存在返回的函数中,这种程序结构称为“闭包(Closure)”。
需要注意的是,返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到被调用了f()才执行。,因此在返回函数中最好不要引用任何循环变量或者后续会发生变化的变量,看个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f) #这里f是函数,fs是个list,所以外函数count()只是返回了一个含有函数f的list fs,就是等同于把一个表达式一个计算方法i*i放进了list中并没有计算!先放进去 for循环三次 放进去3个函数计算公式i*i
return fs # for循环结束才把fs返回给 count(),循环结束的时候i已经变成了3 怎么调用都是3*3
f1, f2, f3 = count()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际输出结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
结果全部都是9。原因就在于返回函数f引用了变量i,但它并非立刻执行。等到3个函数都返回时,等到函数执行的时候,它们所引用的变量i已经变成了3,因此f1,f2,f3的结果都是9。
== 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。==
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
输出结果
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
缺点是代码较长,可利用lambda函数缩短代码。
总结
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
【练习】利用闭包返回一个计数器函数,每次调用它返回递增整数
利用闭包返回一个计数器函数,每次调用它返回递增整数:
# -*- coding: utf-8 -*-
def createCounter():
def counter():
return 1
return counter
# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')
【交作业】
- 错误示范
# -*- coding: utf-8 -*-
def createCounter():
def counter():
n = 0
n = n + 1
return n
return counter
# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')
Run
输出结果
(1, 1, 1, 1, 1)
测试失败!
⚠️注意:返回函数中不要引用任何可能会变化的变量!
看到报错我还疑惑了挺久,凭什么不让我在内函数里修改变量i ?然后发现Python一切皆对象(这点可能和python是动态语言有关?定义变量时不需要像java那样指定类型),包括整数1也是对象,而且最重要的是Python中整形是不可变的,n和n+1指向的是两块不同的地址。在内函数中执行 n = n + 1 时,机器并不知道你是想创建一个局部变量n 呢还是想做赋值操作(创建变量需更换变量名,python 3.x赋值要加nonlocal修饰),于是报错。
这是典型的闭包函数,内部函数引用了外部函数变量,是不允许内部函数随便修改外部变量的。一旦修改了,内部函数就会新生成一个与外部函数同名的变量,而这个变量n,在内部尚未赋值初始值,直接用n=n+1,所以报错了。解决办法,python 3.x 中需要用nonlocal n; 定义以后才可以这么搞
# -*- coding: utf-8 -*-
def createCounter():
n = 0
def counter():
nonlocal n
n = n + 1
return n
return counter
# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')
Run
输出结果
1 2 3 4 5
测试通过!
- 解题思路:
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
传入到counter中的变量不能改变,我们只需要传一个不会变的值进去即可.而这个值不一定是常规的变量.
比如下面的cnt设置成0的话,在counter函数中,我们并不能改变cnt的值,但是如果我们传的cnt是一个列表的话,其实本质上传递的是一个地址,在这个地址上我们可以做任意改变.只要这个列表还是这个列表,他的地址就不会变.利用这种特性,我们就可以随便折腾了.
def createCounter():
cnt = [0] # 将cnt设定为数组
def counter():
cnt[0] = cnt[0]+1 # 修改数组中的元素值
return cnt[0] # 返回修改的元素值
return counter
/) /)
ฅ(• - •)ฅ ~ ~ ฅ’ω’ฅ