高阶函数 Higher-order function
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
函数作为输入:
一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
当我们调用add(-5, 6, abs)
时,参数x
,y
和f
分别接收-5
,6
和abs
函数作为输出
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
调用函数f
时,才真正计算求和的结果:
>>> f()
25
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
f1()
和f2()
的调用结果互不影响。
闭包 Closure
注意到返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
全部都是9
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了3
,因此最终结果为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
深入理解闭包
什么是闭包?闭包有什么用?为什么要用闭包?今天我们就带着这3个问题来一步一步认识闭包。闭包和函数紧密联系在一起,介绍闭包前有必要先介绍一些背景知识,诸如嵌套函数、变量的作用域等概念
作用域
作用域是程序运行时变量可被访问的范围,定义在函数内的变量是局部变量,局部变量的作用范围只能是函数内部范围内,它不能在函数外引用。
def foo():
num = 10 # 局部变量
print(num) # NameError: name 'num' is not defined
定义在模块最外层的变量是全局变量,它是全局范围内可见的,当然在函数里面也可以读取到全局变量的。例如:
num = 10 # 全局变量
def foo():
print(num) # 10
嵌套函数
函数不仅可以定义在模块的最外层,还可以定义在另外一个函数的内部,像这种定义在函数里面的函数称之为嵌套函数(nested function)例如:
def print_msg():
# print_msg 是外围函数
msg = "zen of python"
def printer():
# printer是嵌套函数
print(msg)
printer()
# 输出 zen of python
print_msg()
对于嵌套函数,它可以访问到其外层作用域中声明的非局部(non-local)变量,比如代码示例中的变量 msg
可以被嵌套函数 printer
正常访问。
那么有没有一种可能即使脱离了函数本身的作用范围,局部变量还可以被访问得到呢?答案是闭包
什么是闭包
函数身为第一类对象,它可以作为函数的返回值返回,现在我们来考虑如下的例子:
def print_msg():
# print_msg 是外围函数
msg = "zen of python"
def printer():
# printer 是嵌套函数
print(msg)
return printer
another = print_msg()
# 输出 zen of python
another()
这段代码和前面例子的效果完全一样,同样输出 "zen of python"。不同的地方在于内部函数 printer
直接作为返回值返回了。
一般情况下,函数中的局部变量仅在函数的执行期间可用,一旦 print_msg()
执行过后,我们会认为 msg
变量将不再可用。然而,在这里我们发现 print_msg 执行完之后,在调用 another 的时候 msg 变量的值正常输出了,这就是闭包的作用,闭包使得局部变量在函数外被访问成为可能。
看完这个例子,我们再来定义闭包,维基百科上的解释是:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
这里的 another
就是一个闭包,闭包本质上是一个函数,它有两部分组成,printer
函数和变量 msg
。闭包使得这些变量的值始终保存在内存中。
闭包,顾名思义,就是一个封闭的包裹,里面包裹着自由变量,就像在类里面定义的属性值一样,自由变量的可见范围随同包裹,哪里可以访问到这个包裹,哪里就可以访问到这个自由变量。
为什么要使用闭包
闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
一般来说,当对象中只有一个方法时,这时使用闭包是更好的选择。来看一个例子:
def adder(x):
def wrapper(y):
return x + y
return wrapper
adder5 = adder(5)
# 输出 15
adder5(10)
# 输出 11
adder5(6)
这比用类来实现更优雅,此外装饰器也是基于闭包的一中应用场景。
所有函数都有一个 __closure__
属性,如果这个函数是一个闭包的话,那么它返回的是一个由 cell 对象 组成的元组对象。cell 对象的cell_contents 属性就是闭包中的自由变量。
>>> adder.__closure__
>>> adder5.__closure__
(<cell at 0x103075910: int object at 0x7fd251604518>,)
>>> adder5.__closure__[0].cell_contents
5
这解释了为什么局部变量脱离函数之后,还可以在函数之外被访问的原因的,因为它存储在了闭包的 cell_contents中了。
练习
利用闭包返回一个计数器函数,每次调用它返回递增整数:
def createCounter():
i = 0
def counter():
nonlocal i
i += 1
return i
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('测试失败!')
装饰器 Decorator
装饰器功能是: 将被装饰的函数当作参数传递给与装饰器对应的函数(名称相同的函数),并返回被装饰过后的函数”
如下图
其中 a() 为与装饰器 @a 对应的函数, b() 为装饰器修饰的函数
@a 就是将 b() 传递给 a(),并返回新的 b
举个例子:
def dobi(qf): # 与装饰器@dobi相对应,名称相同
return qf()
@dobi # 装饰器
def qinfeng(): # 被装饰的函数
print('dobi')
上面使用@dobi来表示装饰器,其等同于:qinfeng = dobi(qinfeng)
解析过程是这样子的:
- python 解释器发现@dobi,就去调用与其对应的函数( dobi 函数)
- dobi 函数调用前要指定一个参数,传入的就是@dobi下面修饰的函数,也就是 qinfeng()
- dobi() 函数执行,调用 qinfeng(),qinfeng() 打印“dobi”