高阶函数

  • 变量可以指向函数

    1. >>> f = abs # 函数本身也可以赋值给变量,即:变量可以指向函数。
    2. >>> f(-10) # 直接调用abs()函数和调用变量f()完全相同
    3. 10
  • 函数名也是变量
    函数名其实就是指向函数的变量!
    对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
    注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10

    1. >>> abs = 10 # 把abs指向10后,就无法通过abs(-10)调用该函数了!
    2. >>> abs(-10)
    3. Traceback (most recent call last):
    4. File "<stdin>", line 1, in <module>
    5. TypeError: 'int' object is not callable
  • 传入函数
    既然变量可以指向函数,函数的参数能接收变量,
    那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
    一个最简单的高阶函数:

    1. def add(x, y, f):
    2. return f(x) + f(y)

    把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

map

map()函数接收两个参数,一个是函数,一个是Iterable
map将传入的函数依次作用到序列的每个元素,并把结果作为新的**Iterator**返回。

  1. # 比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上
  2. >>> def f(x):
  3. ... return x * x
  4. ...
  5. >>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) # map()传入的第一个参数是f,即函数对象本身。
  6. >>> list(r) # 结果r是一个Iterator,Iterator是惰性序列
  7. [1, 4, 9, 16, 25, 36, 49, 64, 81] # 因此通过list()把整个序列都计算出来并返回一个list。
  8. >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
  9. ['1', '2', '3', '4', '5', '6', '7', '8', '9']

reduce

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,
reduce把结果继续和序列的下一个元素做累积计算,比方说对一个序列求和,就可以用reduce实现:

  1. >>> from functools import reduce
  2. >>> def add(x, y):
  3. ... return x + y
  4. ...
  5. >>> reduce(add, [1, 3, 5, 7, 9])
  6. 25
  1. # 如果考虑到字符串str也是一个序列,配合map(),我们就可以写出把str转换为int的函数
  2. from functools import reduce
  3. DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
  4. def str2int(s):
  5. def fn(x, y):
  6. return x * 10 + y
  7. def char2num(s):
  8. return DIGITS[s]
  9. return reduce(fn, map(char2num, s))

filter

filter()函数用于过滤序列。接收一个函数和一个序列。

map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

  1. # 例如,在一个list中,删掉偶数,只保留奇数
  2. def is_odd(n):
  3. return n % 2 == 1
  4. list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
  5. # 结果: [1, 5, 9, 15]

sorted

排序算法

Python内置的sorted()函数就可以对list进行排序:

  1. >>> sorted([36, 5, -12, 9, -21])
  2. [-21, -12, 5, 9, 36]

此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

  1. >>> sorted([36, 5, -12, 9, -21], key=abs)
  2. [5, 9, -12, -21, 36]

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

  1. >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
  2. ['Zoo', 'Credit', 'bob', 'about']

返回函数

函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
以求和函数为例:

  1. def lazy_sum(*args):
  2. def sum():
  3. ax = 0
  4. for n in args:
  5. ax = ax + n
  6. return ax
  7. return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

  1. >>> f = lazy_sum(1, 3, 5, 7, 9)
  2. >>> f
  3. <function lazy_sum.<locals>.sum at 0x101c6ed90>
  4. # 调用函数`f`时,才真正计算求和的结果:
  5. >>> f()
  6. 25

注意,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

  1. >>> f1 = lazy_sum(1, 3, 5, 7, 9)
  2. >>> f2 = lazy_sum(1, 3, 5, 7, 9)
  3. >>> f1==f2
  4. False

f1()f2()的调用结果互不影响。

闭包

注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。
另外,返回的函数并没有立刻执行,而是直到调用了f()才执行:

  1. def count():
  2. fs = []
  3. for i in range(1, 4):
  4. def f():
  5. return i*i
  6. fs.append(f)
  7. return fs
  8. # 每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
  9. f1, f2, f3 = count()

你可能认为调用f1()f2()f3()结果应该是149,但实际结果是:全部都是9

原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9

返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

  1. def count():
  2. def f(j):
  3. def g():
  4. return j*j
  5. return g
  6. fs = []
  7. for i in range(1, 4):
  8. fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
  9. return fs
  10. # 缺点是代码较长,可利用lambda函数缩短代码。

小结

一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

匿名函数

关键字lambda表示匿名函数,冒号前面的x表示函数参数。

  1. >>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
  2. [1, 4, 9, 16, 25, 36, 49, 64, 81]
  3. #相当于:
  4. def f(x):
  5. return x * x

匿名函数有个限制,就是只能有一个表达式,不用写return返回值就是该表达式的结果

匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

  1. >>> f = lambda x: x * x
  2. >>> f
  3. <function <lambda> at 0x101c6ef28>
  4. >>> f(5)
  5. 25

同样,也可以把匿名函数作为返回值返回,比如:

  1. def build(x, y):
  2. return lambda: x * x + y * y

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
函数对象有一个__name__属性,可以拿到函数的名字:

  1. >>> def now():
  2. pass
  3. >>> now.__name__
  4. 'now'

假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

  1. def log(func):
  2. def wrapper(*args, **kw):
  3. print('call %s():' % func.__name__)
  4. return func(*args, **kw)
  5. return wrapper
  6. #由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
  7. # wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

  1. @log
  2. def now():
  3. print('2015-3-25')
  4. >>> now()
  5. call now():
  6. 2015-3-25
  7. # 把@log放到now()函数的定义处,相当于执行了语句:
  8. now = log(now)

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

  1. def log(text):
  2. def decorator(func):
  3. def wrapper(*args, **kw):
  4. print('%s %s():' % (text, func.__name__))
  5. return func(*args, **kw)
  6. return wrapper
  7. return decorator

这个3层嵌套的decorator用法如下:

  1. @log('execute')
  2. def now():
  3. print('2015-3-25')
  4. >>> now()
  5. execute now():
  6. 2015-3-25

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

  1. >>> now = log('execute')(now)

我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

  1. >>> now.__name__
  2. 'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

  1. import functools
  2. def log(func):
  3. @functools.wraps(func)
  4. def wrapper(*args, **kw):
  5. print('call %s():' % func.__name__)
  6. return func(*args, **kw)
  7. return wrapper

或者针对带参数的decorator:

  1. import functools
  2. def log(text):
  3. def decorator(func):
  4. @functools.wraps(func)
  5. def wrapper(*args, **kw):
  6. print('%s %s():' % (text, func.__name__))
  7. return func(*args, **kw)
  8. return wrapper
  9. return decorator

偏函数

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2

  1. >>> import functools
  2. >>> int2 = functools.partial(int, base=2)
  3. >>> int2('1000000')
  4. 64
  5. >>> int2('1010101')
  6. 85

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

  1. >>> int2('1000000', base=10)
  2. 1000000

最后,创建偏函数时,实际上可以接收函数对象、*args**kw这3个参数:

  1. int2 = functools.partial(int, base=2)
  2. # 实际上固定了int()函数的关键字参数`base`,也就是:
  3. int2('10010')
  4. # 相当于
  5. kw = { 'base': 2 }
  6. int('10010', **kw)

当传入:

  1. max2 = functools.partial(max, 10)
  2. # 实际上会把`10`作为`*args`的一部分自动加到左边,也就是:
  3. max2(5, 6, 7)
  4. # 相当于
  5. args = (10, 5, 6, 7)
  6. max(*args) # 结果为`10`。