函数是Python内建支持的一种封装,我们通过把大段的代码拆成函数,然后通过函数调用,就可以把复杂任务分解成一个个简单的任务,这种分解可以称之为面向过程的程序设计。 举个例子:比如你要熟练掌握Python,这是你的目标,如果当做一个任务,那这个任务会很复杂,因为你首先要懂Python的基础语法,数据类型、函数、对象等;其次你还要知道不少内建包和常用工具包的使用;再然后呢,你可能还需要懂一些网络编程,数据库开发;如果做web开发的话,还要学习web框架,以及运维相关的知识。

而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

高阶函数

变量可以指向函数。我们以Python内置的求绝对值的函数abs为例:

print(abs(-10))   # abs(-10)是函数调用
 
print(abs)   # abs是函数本身

f = abs
print(f)  # 函数本身也可以赋值给变量

print(f(-10))  # 直接调用abs()函数和调用变量f()完全相同

结论:函数本身也可以赋值给变量,即:变量可以指向函数。

函数名也是变量。那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

abs = 10
print(abs(-10))

把abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,请重启Python交互环境。注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。函数式编程就是指这种高度抽象的编程范式。

def add(x, y, f):
    return f(x) + f(y)

MapReduce是Google提出的一个软件架构,用于处理大规模海量数据。并在之后广泛的应用于Google的各项应用中,2006年Apache的Hadoop项目正式将MapReduce纳入到项目中。

不过接下来我们要学习的是Python函数式编程中常用的内建函数map()和reduce()函数,而不是Google的MapReduce。

map

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

当Iterable只有一个时,将函数作用于这个Iterable的每个元素上,得到一个新的Iterator。我们看下面一张图就可以直观的说明map是如何工作的:

举个例子,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

def f(x):
    return x * x

r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r))

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,因此通过list()函数让它把整个序列都计算出来并返回一个list。

当Iterable有多个时,map()会并行地对每个Iterable执行如下过程:

也就是说,每一个Iterable同一位置的元素在执行过一个多元函数之后,得到一个返回值,这些返回值放在一个结果列表中。
还是刚才那个例子,只不过我们拆分成三个序列,然后使用map进行计算:

def f(x,y,z):
    return x * y * z

r = map(f, [1, 2, 3],[4, 5, 6],[ 7, 8, 9])
print(list(r))      #  [28, 80, 162]

上面是返回值是一个值的情况,实际上也可以是一个元组。我们改一改刚才的例子:

def f(x,y,z):
    return x * y * z, x+y+z

r = map(f, [1, 2, 3],[4, 5, 6],[ 7, 8, 9])
print(list(r))      #  [(28, 12), (80, 15), (162, 18)]

好了,看例子很明白了吧。但是会不会有这样的问题,如果传入的Iterable长度不一样怎么办呢?使用多个迭代器时,当最短迭代器耗尽时,迭代器停止。

def f(x,y,z):
    return x * y * z, x+y+z

r = map(f, [1, 2, 3, 4],[4, 5, 6,7],[ 7, 8, 9])
print(list(r))    #  [(28, 12), (80, 15), (162, 18)]

嗯,结果跟上一个一样。

reduce

reduce函数即为化简,它是这样一个过程:每次迭代,将上一次的迭代结果(第一次时为init的元素,如没有init则为seq的第一个元素)与下一个元素一同执行一个二元的函数。请注意,在reduce函数中,init是可选的,如果使用,则作为第一次迭代的第一个元素使用。

举个例子,比方说对一个序列求和,就可以用reduce实现:

from functools import reduce
def add(x, y):
    return x + y

reduce(add, [1, 3, 5, 7, 9])

请注意啊,Python3里面使用reduce需要添加from functools import reduce,原因是reduce在_functools模块里面,而前面讲到map则在builtins,在builtins里面是不需要显示引入的。但是他们都属于built-ins的函数。

结合map函数,我们可以写一个字符串转数字的函数:

from functools import reduce

DIGITS = {'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 + y
    def char2num(s):
        return DIGITS[s]
    return reduce(fn, map(char2num, s))

filter

Python内建的filter()函数用于过滤序列。

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

例如:在一个list中,删掉偶数,只保留奇数,可以这么写:

def is_odd(n):
    return n % 2 == 1
 
newlist = list(filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
print(newlist)

比如:把一个序列中的空字符串删掉,可以这么写:

def not_empty(s):
    return s and s.strip()

newlist = list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
print(newlist)

过滤出1~100中平方根是整数的数:

import math
def is_sqr(x):
    return math.sqrt(x) % 1 == 0
 
newlist = list(filter(is_sqr, range(1, 101)))
print(newlist)

sort

Python内置的sorted()函数能够进行排序。

s = sorted([2, 11, -23, -9, 51])
print(s)

sorted()是一个高阶函数,接受一个key函数来实现自定义的排序,例如按绝对值大小排序:

s = sorted([2, 11, -23, -9, 51], key=abs)
print(s)

对字符串进行排序:

s = sorted(['Yisa','tom','Joe'], keys = str.lower)
print(s)

sorted()的可以传入参数:reverse=True 取反向排序

s = sorted(['Yisa','tom','Joe'], keys = str.lower, reverse=True)
print(s)

返回函数

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

我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

但是如果我不需要立刻就求和然后返回值,而是等我需要的时候在计算,怎么办呢?我们可以返回一个函数:

def wait_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

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

f = wait_sum(8,2,3,5)
print(f)

调用函数f时,才真正计算求和的结果:

print(f())

我们在函数wait_sum中又定义了函数sum,并且内部函数sum可以引用外部函数wait_sum的参数和局部变量,当调用wait_sum()返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)

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

f1 = wait_sum(1, 3, 5, 7, 9)
f2 = wait_sum(1, 3, 5, 7, 9)
print(f1==f2)

闭包

定义:如果内部函数引用外部函数的参数和局部变量,并且内部函数被返回的这种结构就称为闭包。

注意到返回的函数在其定义内部引用了局部变量,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

如果调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:

print(f1())
print(f2())
print(f3())

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

如果想避免这种情况:返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量!!!