首页 > Python笔记 阅读数:17

Python函数式编程(map、sorted、filter、reduce)完全攻略

我们先来简单说明函数式编程(Functional Programming)的概念。之前讲过,函数就是完成某种功能的代码段,这个解释基本上就是同义词重复,但也道出了函数的本质。

通常,我们会把大段代码拆成不同的功能块(即函数),这些功能块类似于积木的模块,通过层层调用,像搭积木一般,我们就可以把复杂的任务分解成简单的任务。这种“分而治之”的模块化思想,就是面向过程程序设计的思想。可以说,函数就是面向过程编程范式的基本单元。

Python 内部有大量内置函数,用户也可以自己设计自定义的函数。而我们知道,Python 是一门纯粹的面向对象的编程语言。因此,函数在 Python 中是以对象的形式出现的。

而对象是可以作为函数参数和返回值进行传递的。函数式编程的一个显著特征就是,它允许把函数本身作为参数传入另一个函数(对象),还允许返回一个函数(对象)。

函数式编程虽然也可以归属到面向过程程序设计的范畴当中,但其思想更接近数学计算。简单来讲,函数式编程是一种“广播式”编程,我们可通过前面提到的 lambda 定义简易函数,将其用在科学计算中,让代码显得更加简单。

在 Python 中,函数式编程在 lambda 表达式、filter( )、map( ) 和 reduce( ) 中得到了很好的体现。lambda 表达式已经介绍过,下面来说明它与其他函数如何配合使用,我们将结合几个好用的内置函数 filter( )、map( ) 和 reduce( ),来说明 lambda 表达式的妙用之处。

C++ filter()函数

我们先来讨论 filter( ) 函数的使用。顾名思义,filter( ) 函数是一个过滤器,作用于可迭代的数据序列,根据它的第一个参数(一个指定规则的函数对象)可筛选出符合条件的元素,凡是不符合条件的都会被淘汰出局。

该函数的原型如下:

filter(function, iterable)
参数:
function --判断函数
iterable --可迭代对象
返回值:
返回一个迭代器对象


filter( ) 中有两个参数,第一个为函数,用于制定筛选规则,第二个为序列,为第一个参数数据。序列中的每个元素都将作为参数传递给函数进行判断,符合条件的(即判定为 True 的)留下,否则就“淘汰出局”。示例代码如下:
In [6]: def fun (variable) : #自定义函数:过滤非元音字母
   ...:     letters = ['a', 'e', 'i', 'o', 'u']
   ...:     if (variable in letters):
   ...:         return True
   ...:     else:
   ...:         return False
   ...:
In [7]: sequence =['g', 'f', 'e', 'j', 'k', 's', 'p', 'k', 'o']
In [8]: filtered = filter(fun, sequence)
In [9]: filtered #并不能直接输出筛选数据
Out[9]: <filter at 0xl0649acl0>
In [10]: list (filtered) #先转换为列表,再次输出
Out[10]: ['e', 'o']
在 Python 3.x 中,filter( ) 函数返回一个迭代器对象(参见 In [9] 处),该对象并不能直接使用。这时,我们可以使用内置函数 list( ) 将其转换为列表(参见 In [10] 处),然后再正常输出。

如果 filter( ) 函数的第一个参数(即评估函数)功能非常简单,能够通过一行代码表达,还可以用 lambda 表达式来描述,示例代码如下:
In [11]: a_List = [2, 18, 9, 22, 17, 24, 8, 12, 27]
In [12]: data = filter(lambda x: x % 3 ==0, a_List)
In [13]: newlist = list(data)
In [14]: print(newlist)
[18, 9, 24, 12, 27]

C++ map()函数

如前所述,在 Python 中,函数也是一个对象,对象是可以作为函数参数的。map( ) 函数中的第 1 个参数就是一个函数对象——filter( ) 函数。map( ) 函数会根据这个参数制定的规则将一个可迭代对象(某种序列数据)转换为另一个序列。由于原序列中的元素和被转换序列中的元素存在一一对应的关系,因此我们也将这种关系描述为“映射”(map)。

其函数原型如下:

map(function, iterable, ...)
参数:
function --映射函数
iterable -- 一个或多个可迭代的序列 返回值:
Python 3.x 返回迭代器


map( ) 函数中第 2 个(含)以后的参数表示待加工的数据序列,具体需要几个数据序列,取决于第 1 个参数。如前所述,第 1 个参数是函数对象,那么它就像一个加工机器,需要几样“原材料”,其后就跟随几个可迭代的数据序列。

比如说,第 1 个参数对应的函数功能是取某个序列中元素的相反数,“取反”是一元操作,那么第 1 个参数之后再跟一个序列参数即可。再比如,第 1 个参数对应的函数功能是求和,“求和”是二元操作,那么第 1 个参数之后就需要两个序列,以此类推。

在第 1 个参数代表的函数加工下,第 2 个(含)参数代表的数据源就会转换成一个新的序列。下面举例说明。
In [15]: def myfunc(n):   #自定义一个函数
    ...:     return len(n)
    ...:
In [16]: word_len = map(myfunc, ('apple', 'banana', 'cherry', 'carmel'))
In [17]: word_len #这个生成的可迭代对象并不能直接输出
Out[17]: <map at 0xl06417dd0>
In [18]: list(word_len) #转换为列表输出
Out[18]: [5, 6, 6, 6] 

需要注意的是,在调用 map( ) 函数时,它的第 1 个参数为函数对象,但此时仅仅需要给出函数的名称,无须添加函数后面的那对圆括号。类似于 filter( ) 函数,在 Python 3.x 中,map( ) 函数返回的也是一个迭代器对象(在后面的章节中我们会详细讲解迭代器的用法),我们可使用内置函数 list( )将其转换为常用的列表(参见 In [18] 处),然后再正常输出。

自然,如果这个映射函数(即 map( ) 的第 1 个参数)本身并不复杂,而我们也不想操心为这个函数取名,lambda 表达式就有用武之地了。示例代码如下:
In [19]: a_List = [2, 18, 9, 22, 17, 24, 8, 12, 27]
In [20]: my_map = map(lambda x : x*2 + 1, a_List)   #对序列中的每个元素x 2再+1
In [21]: newlist2 = list(my_map)  #转换为列表
In [22]: newlist2    #正常输出
Out[22]: [5, 37, 19, 45, 35, 49, 17, 25, 55]

以上示例都有一个共同的特征,map( ) 函数第一个参数(即函数对象)所执行的操作都是一元操作,因此第一个参数之后只有一个序列。下面我们给出一个二元操作的示例:
In [23]: def myfunc (a, b): #自定义一个二元操作函数
    ...: return a + b
    ...:
In [24]: str_cat = map(myfunc, ('apple ', 'banana ', 'I love ') , ('orange', 'lemon', 'carmel'))
In [25]: list(str_cat) #输出验证
Out[25]: ['apple orange', 'banana lemon', 'I love carmel']
在 In [23] 处,我们定义了一个二元操作,所以在 In [24] 处的 map( ) 函数中,一个参数之后跟了两个数据序列。

如前所述,在函数定义时,无须定义形参的类型,形参类型由正在调用中的实参决定。由于 myfunc 参数之后的两个序列中的元素为字符串,所以在 In [23] 处,形参 a 和 b 被实例化为字符串类型,而用加号+对两个字符串类型实施操作,实际上就是连接这两个字符串。因此就有了 Out[25] 处的结果。

显然,如果 myfunc 参数之后的序列是两个数值型的序列,那就可以将两个序列的对应元素相加,示例代码如下:
In [26]: num_add = map(myfunc,[1, 2, 3], [4, 5, 6])
In [27]: list(num_add)
Out[27]: [5, 7, 9]

从上面的代码中可以再次感受到,Python 在定义函数时不指定函数形参的类型,在某种程度上实现了函数的泛型程序设计(generic programming)。泛型很有作用,它允许程序员在编写代码时不提前设定参数类型,在实例化时再“因地制宜”确定这些参数的具体类型,以达到“以不变应万变”的泛化作用。

或许有读者会问,上述 map( ) 函数的功能好像用列表推导式也能完成,为什么还要专门设计一个 map( ) 函数呢?是的,通过列表推导式的确也能完成类似功能。map( ) 函数的出现,主要是对性能的考量。列表推导式虽然代码简单,但在本质上,它就是一个简化版的 for 循环,Python 中的 for 循环效率并不高,而通过 map( ) 函数实现相同的功能时效率要高很多。

C++ reduce()函数

本节我们来讨论一下 reduce( ) 函数的使用。“reduce”的本意就是“规约”或“减少”。reduce( ) 函数会对参数序列中的元素按照一定的规则进行“规约”,从而将数据减少到只有一个累计的数值。

如前所述,函数式编程的一个具体体现就是,一个函数可以作为另外一个函数的参数。上面提及的“一定的规则”就是指一个实现特定功能的函数,它将作为 reduce( ) 函数的一个参数。

reduce( ) 函数的原型如下:

reduce(function, iterable [, initializer])
参数:
function --实现特定规约功能的函数,它是一个二元函数
iterable --可迭代数据对象
initializer --可选项,规约操作时可能用到的初始参数
返回值:
返回函数计算结果


具体来说,reduce( ) 函数对一个可迭代数据集合(如列表、元组等)中的所有数据执行下列操作:对于可迭代对象 iterable(reduce( ) 函数的第 2 个参数)的前两个元素,利用 function(reduce( ) 函数的第一个参数)所代表的规则进行计算,得到的结果再与 iterable 对象中的第 3 个数据拼接为一对,然后再利用 function 所示的约减规则进行运算,得到下一个结果……以此类推,直到计算出最后一个结果。

示例代码如下。
In [28]: from functools import reduce
In [29]: reduce (lambda x, y: x + y, [1,2,3,4,5]) # 使用丄 ambda 表达式
Out[29]: 15

这里需要注意的是,在 Python 3.x 中,reduce()函数已经被从全局命名空间里移除了,它现在被放置在 fucntools 模块里,如果想要使用它,需要事先通过导入 functools 模块来调用 reduce( ) 函数(参见 In [28])。

显然,如果我们能很好地利用 reduce( ),便可以代替 for 循环做很多工作。例如,如果想计算“1+2+3+…+100”的和,我们只需要用下面一条语句即可完成任务。
In [30]: reduce(lambda x, y: x + y, range(1,101))
Out[30]: 5050

此外,我们还可以借助 reduce( ) 函数,求得某个序列的最大值或最小值,示例代码如下:
In [31]: a_list = [1 , 3, 5, 6, 2]
In [32]: reduce(lambda a,b : a if a > b else b,a_list)
Out [32]: 6

C++ sorted()函数

在算法设计中,我们常用到排序功能。如果比较的对象是数字,则可直接比较。但如果比较的对象是字符串或者字典,那么直接比较大小得到的结果可能是没有意义的。因此,我们需要利用特定函数指定排序标准,也就是说,让某个函数成为排序函数的参数,这正是函数式编程的用武之地。

在 Python 中,利用内置的 sorted( ) 函数可实现对列表的排序,示例代码如下:
In [33]: sorted([70, 60, -20, 10, -30])
Out[33]: [-30, -20, 10, 60, 70]

sorted( ) 函数默认是按升序来排序的。当然,我们也可以利用它的 reverse 参数改变这一默认设置,示例如下:
In [34]: sorted([70, 60, -20, 10, -30], reverse = True)
Out[34]: [70, 60, 10, -20, -30]

上面的需求都很简单,十分容易达成。但如果我们想根据列表中元素的绝对值进行排序,sorted( ) 函数的默认规则就无能为力了。这时需要启用 sorted( ) 的另外一个参数 key,它可以接收一个外部函数名作为参数,然后列表中所有元素都需要一一被这个函数加工一番,作为排序索引的依据。

根据上述需要,我们可以用求绝对值的内置函数 abs( ) 充当 key 参数的实参。需要注意的是,当某个函数充当 key 的实参时,仅仅给出函数名即可,函数名后面的那对圆括号( )不需要,且函数参数也无须指定,因为 sorted( ) 函数的第 1 个参数(即可迭代对象)中的元素,会逐个充当key所指定函数的实参,然后 sorted( ) 函数会根据 key 所指定函数的返回结果(实际上是新的可迭代对象)进行排序。
In [35]: sorted([70, 60, -20, 10, -30], key = abs, reverse=True)
Out[35]: [70, 60, -30, -20, 10]

需要注意的是,sorted( ) 函数返回一个新列表,它不会对原有列表造成任何影响。读者朋友们可以用一个列表变量赋值,如 L = [70, 60, -20, 10, -30],然后把 L 传入 sorted( ) 函数,分别查看排序前后 L 中的内容是否发生变化。你会发现,排序后的 L 依然如初。

如果想让 L 内部的元素发生实质性的排序变化,该怎么办呢?这时就要利用列表对象本身的 sort( ) 函数,示例代码如下:
In [36]: L = [70, 60, -20, 10, -30]
In [37]: L.sort(key = abs, reverse = True)
In [38]: L     #输出验证
Out[38]: [70, 60, -30, -20, 10]

相关文章