高阶函数
变量可以指向函数
>>> f = abs # 函数本身也可以赋值给变量,即:变量可以指向函数。>>> f(-10) # 直接调用abs()函数和调用变量f()完全相同10
函数名也是变量
函数名其实就是指向函数的变量!
对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10。>>> abs = 10 # 把abs指向10后,就无法通过abs(-10)调用该函数了!>>> abs(-10)Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: 'int' object is not callable
传入函数
既然变量可以指向函数,函数的参数能接收变量,
那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:def add(x, y, f):return f(x) + f(y)
把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。
map
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的**Iterator**返回。
# 比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上>>> def f(x):... return x * x...>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) # map()传入的第一个参数是f,即函数对象本身。>>> list(r) # 结果r是一个Iterator,Iterator是惰性序列[1, 4, 9, 16, 25, 36, 49, 64, 81] # 因此通过list()把整个序列都计算出来并返回一个list。>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,比方说对一个序列求和,就可以用reduce实现:
>>> from functools import reduce>>> def add(x, y):... return x + y...>>> reduce(add, [1, 3, 5, 7, 9])25
# 如果考虑到字符串str也是一个序列,配合map(),我们就可以写出把str转换为int的函数from functools import reduceDIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}def str2int(s):def fn(x, y):return x * 10 + ydef char2num(s):return DIGITS[s]return reduce(fn, map(char2num, s))
filter
filter()函数用于过滤序列。接收一个函数和一个序列。
和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
# 例如,在一个list中,删掉偶数,只保留奇数def is_odd(n):return n % 2 == 1list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))# 结果: [1, 5, 9, 15]
sorted
排序算法
Python内置的sorted()函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21])[-21, -12, 5, 9, 36]
此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)[5, 9, -12, -21, 36]
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)['Zoo', 'Credit', 'bob', 'about']
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
以求和函数为例:
def lazy_sum(*args):def sum():ax = 0for n in args:ax = ax + nreturn axreturn sum
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9)>>> f<function lazy_sum.<locals>.sum at 0x101c6ed90># 调用函数`f`时,才真正计算求和的结果:>>> f()25
注意,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)>>> f2 = lazy_sum(1, 3, 5, 7, 9)>>> f1==f2False
f1()和f2()的调用结果互不影响。
闭包
注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。
另外,返回的函数并没有立刻执行,而是直到调用了f()才执行:
def count():fs = []for i in range(1, 4):def f():return i*ifs.append(f)return fs# 每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。f1, f2, f3 = count()
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:全部都是9!
原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
def count():def f(j):def g():return j*jreturn gfs = []for i in range(1, 4):fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()return fs# 缺点是代码较长,可利用lambda函数缩短代码。
小结
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
匿名函数
关键字lambda表示匿名函数,冒号前面的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]#相当于:def f(x):return x * x
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x>>> f<function <lambda> at 0x101c6ef28>>>> f(5)25
同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y):return lambda: x * x + y * y
装饰器
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
函数对象有一个__name__属性,可以拿到函数的名字:
>>> def now():pass>>> now.__name__'now'
假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func):def wrapper(*args, **kw):print('call %s():' % func.__name__)return func(*args, **kw)return wrapper#由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。# wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@logdef now():print('2015-3-25')>>> now()call now():2015-3-25# 把@log放到now()函数的定义处,相当于执行了语句:now = log(now)
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
def log(text):def decorator(func):def wrapper(*args, **kw):print('%s %s():' % (text, func.__name__))return func(*args, **kw)return wrapperreturn decorator
这个3层嵌套的decorator用法如下:
@log('execute')def now():print('2015-3-25')>>> now()execute now():2015-3-25
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
>>> now = log('execute')(now)
我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper':
>>> now.__name__'wrapper'
因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:
import functoolsdef log(func):@functools.wraps(func)def wrapper(*args, **kw):print('call %s():' % func.__name__)return func(*args, **kw)return wrapper
或者针对带参数的decorator:
import functoolsdef log(text):def decorator(func):@functools.wraps(func)def wrapper(*args, **kw):print('%s %s():' % (text, func.__name__))return func(*args, **kw)return wrapperreturn decorator
偏函数
functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
>>> import functools>>> int2 = functools.partial(int, base=2)>>> int2('1000000')64>>> int2('1010101')85
所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:
>>> int2('1000000', base=10)1000000
最后,创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数:
int2 = functools.partial(int, base=2)# 实际上固定了int()函数的关键字参数`base`,也就是:int2('10010')# 相当于kw = { 'base': 2 }int('10010', **kw)
当传入:
max2 = functools.partial(max, 10)# 实际上会把`10`作为`*args`的一部分自动加到左边,也就是:max2(5, 6, 7)# 相当于args = (10, 5, 6, 7)max(*args) # 结果为`10`。
