参考原文
返回函数
我们已经知道了高阶函数可以接受函数作为参数外,还可以把函数作为结果值返回。我们来看一个实现可变参数的求和:
def calc_sum(*args): ax = 0 for n in args: ax += n return ax
但是,但我们不需要立即知道求和的结果,而是在后面的代码中根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args): def sum(): ax = 0 for n in args: ax += n return ax return sum
当我们调用lazy_sum()时,返回的不是求和结果而是求和函数,当我们调用函数f时,才真正计算求和的结果:
f = lazy_sum(1,2,3,4) print(f) # result <function lazy_sum.<locals>.sum at 0x0000025503463AE8> print(f()) #result 10
闭包
在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数sum()中,这就是“闭包(Closure)”。
注意:当我们调用lazy_sum()时,每次调用都会返回一个新的函数即使传入相同的参数:
f1 = lazy_sum(1,2,3,4) f2 = lazy_sum(1,2,3,4) print(f1==f2) #result False
Tips:当一个函数返回一个函数后其内部的局部变量还被新函数所引用,所以闭包用着简单,但实现可不容易。返回的函数也不是立即执行的,直到调用返回的函数,才会执行内部的代码。
但此时我们不一定理解清楚了"闭包",不信来看个例子:
def count(): fs = [] for i in range(1, 4): def f(): return i * i fs.append(f) return fs f1, f2, f3 = count() print(f1()) print(f2()) print(f3())
在上面的例子中,每次循环都创建了一个新的函数,然后把创建的3个函数都返回了。你可能认为调用f1(),f2()和f3()的结果应该是1, 4, 9 但实际结果却是:9, 9 ,9。为什么呢?原因在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,所以结果是9。
Tips:返回闭包时要牢记一点:返回函数不要引用任何的循环变量,或者后续会发生变化的变量。
但是如果我们一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,这样,无论该循环变量后续任何更改,已经绑定到函数参数的值不变:
def count(): def f(j): def g(): return j * j return g fs = [] #容器用来存储函数 for i in range(1, 4): fs.append(f(i)) return fs f1, f2, f3 = count() #等价f1=count()[0], f2=count()[1], f3=count()[2] print(f1(), f2(), f3()) #result 1 4 9
Tips:一个函数可以返回一个计算结果,也可以返回一个函数;返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会发生的变量。
匿名函数
为什么要使用匿名函数?匿名函数没有名字,不需要显式定义函数,不必担心函数名冲突,使用起来更加的方便。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)函数外,还可以直接传入匿名函数:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) [1, 4, 9, 16, 25, 36, 49, 64, 81]
通过对比可以看出,匿名函数lambda x: x * x实际上就是:
def f(x): return x * x
关键字lambda表示匿名函数,冒号前面的x表示参数。
Tips:匿名函数只有一个表达式,不用写return,返回值就是该表达式的结果。除此之外,和其它函数一样也可以赋值给变量,也可以作为返回值返回。Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。
装饰器
装饰器本质上是一个返回函数的高阶函数,它可以让其他函数在不需要做任何代码变动下增加额外的功能:包括插入日志、性能测试、事务处理、缓存、权限校验等,这种在代码运行期间动态增加功能的方式,称之为装饰器(Decorator)。来看一个打印日志的decorator的定义:
def log(func): #接收一个函数func作为参数--高阶函数的特性 def wrapper(*args, **kw): #可以接受任意参数的调用 print('call %s():' % func.__name__) #func.__name__ 可以拿到函数额名字 return func(*args, **kw) return wrapper
已经定义好了一个decorator,怎么用呢?我们要借助Python的@语法,把decorator置于函数的定义处:
@log def now(): print('2018-04-19')
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
now() ''' call now(): 2018-04-19 '''
@log放到now()函数的定义处,相当于执行了语句:
now = log(now)
解释:由于log()是一个decorator,返回一个函数,所以原来的now()函数仍然存在,只是现在同名的now指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
再来看一种情况:若decorator的本身就需要传入参数的话,又该如何实现呢?此时,就应该编写一个返回decorator的高阶函数。如,要自定义log的文本:
def log(text): def decorator(func): def wrapper(*args, **kw): print('%s %s():' % (text,func.__name__)) return func(*args, **kw) return wrapper return decorator
这个3层嵌套的decorator的用法如下:
@log('DefineText') def now(): print('2018-04-19')
执行结果如下:
now() ''' DefineText now(): 2018-04-19 '''
和两层嵌套的decorator相比,3层嵌套的decorator效果是这样:
now = log('DefineText')(now)
剖析:首先执行log(‘DefineText’),返回的是decorator函数,再调用返回的decorator函数,参数是now函数,最后返回值是wrapper函数。
注意:函数也是对象,它有__name__等属性,但你看经过上面decorator装饰后的函数,它们的__name__已经从原来的'now'变成了'wrapper'。
print(now.__name__) # wrapper
哦,原来是因为最后返回的函数wrapper()函数名字‘wrapper’替换了原来的‘now’,这样有可能依赖函数签名的代码执行可能会出错,所以应该把__name__属性改回去,幸好Python内置的functools.wraps可以帮我们实现,所以一个完整的decorator的写法如下:
#不带参数 import functools def log(func): @functools.wraps(func) def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) return wrapper #带参数 import functools def log(text): def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): print('%s %s():' % (text,func.__name__)) return func(*args, **kw) return wrapper return decorator
小结:在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现。而Python除了能支持OOP的decorator外,直接从语法层次支持decorator,可以用函数实现,也可以用类实现。
偏函数(Partial function)
在前面说过,通过设定函数的默认值,可以降低函数调用的难度,偏函数可以为已知的函数设定参数的默认值。来看看怎么操作:
>>> import functools >>> int2 = functools.partial(int, base=2) #functools.partial帮助我们创建偏函数 >>> int2('1000000') 64
注意到上面的int2函数,仅仅是把base参数设定默认值为2,但也可在函数调用时传入其它的值:
>>> int2('1000000', base=10) 1000000
最后注意,创建偏函数时,实际可以接受函数对象、*args、和**kw、3个参数。如上面的创建的int2函数,实际上固定了int()函数的关键字参数base,也就是:
int2('10010')
相当于这样:
kw = { 'base': 2 } int('10010', **kw)
Tips:当函数的参数个数太多时,需要简化时,使用functools.partial创建一个新的函数,这个新函数固定了原函数的部分参数,使调用时更加的简洁。