高阶函数三剑客:map、filter、reduce全解析
从到高阶函数
在这一次的上一节课之中, 我们是学习了表达式的那些知识内容, 那些可以被写成一行的小函数在实际使用过程里面是让人感觉非常爽快的。不过通常情况是需要和那些高阶函数来配合着使用起来的, 这才能够发挥出它们所具备的真正威力。
高阶函数, 这个东西到底是什么样子呢?
从一种比较通俗的角度来进行说明的话, 所谓的高阶函数, 指的就是那种能够接收作为参数形式存在的其他函数的函数。
这句话可能会让人觉得听起来有点绕弯子, 我决定去换一个说法。
在这部分里, 被公认为是非常经典的三个高阶函数, 它们分别是map()、()、()。
map()这个功能, 它是专门用来处理那些大量的数据, 让它们发生改变的专家。
你的这种情况, 就是你手头拿着一个装满了数字的那些个列表。
1, 2, 3, 4, 5
,想把所有数字都平方。
有一种不使用map这种方法的实现方式, 如下所示:
numbers = [1, 2, 3, 4, 5] squared = [] for n in numbers: squared.append(n ** 2) print(squared) # [1, 4, 9, 16, 25]用map的写法:
numbers = [1, 2, 3, 4, 5] squared = list(map(lambda n: n ** 2, numbers)) print(squared) # [1, 4, 9, 16, 25]用一行代码就可以搞定, 这也就是 map 的魅力所在。
我们来看一下这个叫作map的函数的基本使用方式。
map(function, iterable)map()实战案例
把这个案例一的这个处理方式, 去给到那些个全部的名字, 要把它们都变成大写的。
names = ["alice", "bob", "charlie"] upper_names = list(map(str.upper, names)) print(upper_names) # ['ALICE', 'BOB', 'CHARLIE']案例编号为2的情况, 涉及从字典这个数据结构里面, 去提取某个具体的值。
students = [ {"name": "小明", "score": 85}, {"name": "小红", "score": 92}, {"name": "小张", "score": 78} ] scores = list(map(lambda s: s["score"], students)) print(scores) # [85, 92, 78]这是案例三, 关于对多个列表进行同时处理的操作。
list1 = [1, 2, 3] list2 = [10, 20, 30] result = list(map(lambda x, y: x + y, list1, list2)) print(result) # [11, 22, 33]请注意这个情况, 就是map()函数是允许接收多个可迭代对象的存在的, 不过前提是里面的函数参数必须要进行对应才可以。
():数据过滤小能手
具体的场景内容是, 你手里拿着一个数字组成的列表, 然后你的目的是从这个列表当中去挑出所有的偶数。
不用的写法:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens = [] for n in numbers: if n % 2 == 0: evens.append(n) print(evens) # [2, 4, 6, 8, 10]用的写法:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens = list(filter(lambda n: n % 2 == 0, numbers)) print(evens) # [2, 4, 6, 8, 10]()的基本用法
filter(function, iterable)这个关键的地方在于, 它会留下来所有那个函数返回了True的元素, 而那些会返回False的元素则会被它们过滤掉。
()实战案例
在第一个案例里面, 我们需要把那些内容为空的字符串给过滤掉。
words = ["hello", "", "world", "", "python", ""] non_empty = list(filter(bool, words)) print(non_empty) # ['hello', 'world', 'python']当bool这个函数执行操作的时候, 如果它面对的是一个空的字符串, 那么它就会把这个空的字符串转换成False, 换句话说, 如果它面对的不是一个空的字符串, 也就是非空字符串的情况, 它会把这种情况转换成True。
案例2:筛选出长度大于5的单词
words = ["cat", "dog", "elephant", "bird", "rhinoceros"] long_words = list(filter(lambda w: len(w) > 5, words)) print(long_words) # ['elephant', 'rhinoceros']案例3:过滤出及格的学生
students = [ {"name": "小明", "score": 85}, {"name": "小红", "score": 92}, {"name": "小张", "score": 58} ] passed = list(filter(lambda s: s["score"] >= 60, students)) print(passed) # [{'name': '小明', 'score': 85}, {'name': '小红', 'score': 92}]():累计计算高手
这个 () 和其他两个情况不太一样, 因为该函数没有包含在内置函数之中, 所以必须通过从相关模块进行导入的方式来实现使用。
你的这种情况, 就是你手头拿着一个装满了数字的那些个列表。
1, 2, 3, 4, 5
,想求所有数字的乘积。
不用的写法:
numbers = [1, 2, 3, 4, 5] product = 1 for n in numbers: product *= n print(product) # 120用的写法:
from functools import reduce numbers = [1, 2, 3, 4, 5] product = reduce(lambda x, y: x * y, numbers) print(product) # 120()的基本用法
from functools import reduce reduce(function, iterable[, initializer])()是怎么工作的?
1, 2, 3, 4, 5
求乘积举例:
先取前两个元素, 进行相乘操作, 也就是让一乘以二, 得到结果是二, 然后再拿这个结果与第三个元素进行计算, 即让二乘以三, 得出的结果为六, 接着再取这个结果与第四个元素结合, 计算其乘积, 也就是六乘以四, 得到的值是二十四, 最后把上次的结果与第五个元素进行运算, 即让二十四乘以五, 最终返回的计算结果是一百二十。
这就相当于是那个滚雪球的状况, 它是越滚起来越大。
()实战案例
案例1, 求最大值, 虽然用max()会更简单一些, 拿来理解一下。
from functools import reduce numbers = [3, 7, 2, 9, 4] max_num = reduce(lambda x, y: x if x > y else y, numbers) print(max_num) # 9这个案例的内容是关于将字符串进行拼接的这个操作。
from functools import reduce words = ["Hello", " ", "World", "!"] sentence = reduce(lambda x, y: x + y, words) print(sentence) # Hello World!案例3, 这个案例是用来计算阶乘的。
from functools import reduce import operator n = 5 factorial = reduce(operator.mul, range(1, n + 1)) print(factorial) # 120解释说明的情况是, 这里使用的 .mul 这种写法指的其实就是乘法函数, 它在意思上完全等于 x, y: x * y。
三个函数对比
函数
作用
返回值
典型场景
map()
对 each元素应用函数
变换后的元素集合
把数据进行清理, 然后再进行格式上的转换操作。
()
过滤满足条件的元素
子集
对数据进行筛选, 然后进行去重操作。
()
累计计算
就是指单个值。
求和、求积、拼接
组合使用:威力加倍
用户可以把这三个函数进行混合操作, 这样的话, 就能应对比较复杂的问题。
案例:找出所有偶数的平方和
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 方法1:分步来 evens = filter(lambda n: n % 2 == 0, numbers) squared = map(lambda n: n ** 2, evens) total = sum(squared) print(total) # 220 # 方法2:链式调用 total = sum(map(lambda n: n ** 2, filter(lambda n: n % 2 == 0, numbers))) print(total) # 220虽然那个叫做链式调用的东西看起来很酷, 但是在阅读的时候稍微差了那么一点点意思。我比较建议大家分成一个一个的步骤去写代码这个样子, 这样子在调试的过程当中会方便一点。
列表推导式 vs map/
你很可能就会提出这样的疑问, 也就是你可能会问我说, 我难道不能够采用列表推导式这种方式来完成 map 和 这两个操作的功能吗, 既然存在这种替代方案, 那又为什么还需要去专门学习呢。
确实, 很多场景下<强>列表推导式更直观:
# 用map squared = list(map(lambda n: n ** 2, numbers)) # 用列表推导式 squared = [n ** 2 for n in numbers] # 用filter evens = list(filter(lambda n: n % 2 == 0, numbers)) # 用列表推导式 evens = [n for n in numbers if n % 2 == 0]那为什么还要去好好学习一下名叫map的这个东西呢?
如果你手头已经有一些现成的函数可以使用, 那么进行函数复用的操作会非常简便, 同时你可以利用map工具来更直接地针对多个列表进行同步处理, 因为该工具具备一个特别之处, 就是它可以接收好几个可以被迭代访问的对象作为输入项, 另外值得一提的是惰性求值这一特性, 它会让返回的结果变成一个迭代器形式的数据结构, 这样一来能够大大节省内存空间的占用情况, 这一点在面对庞大的数据量需要处理的时候显得极为重要, 最后在代码风格方面, 在一些特定的情境下采取函数式的编程思维通常会显得更加优雅。
我的建议是, 在场景比较简单的情况下使用列表推导式, 而在场景比较复杂的情况下就使用map函数, 至于最终选择哪种方式, 那就完全取决于你自己的个人喜好。
()的替代方案
很多程序员都不喜欢(), 因为觉得可读性是比较差的。
同样的累加功能, 能够采用更为直接可见的一种表现形式。
from functools import reduce numbers = [1, 2, 3, 4, 5] # 用reduce product1 = reduce(lambda x, y: x * y, numbers) # 用循环 product2 = 1 for n in numbers: product2 *= n # 用标准库 import operator product3 = reduce(operator.mul, numbers) # 用第三方库(需要安装numpy) # import numpy as np # product4 = np.prod(numbers)我的看法是, 这个说法存在它自己所适用的具体环境情况, 但是绝对不能够去随意地过度使用。如果说代码的阅读清晰程度非常糟糕的话, 那就应该去改换成其他不同的处理办法或者技术手段来进行处理。
我曾经踩过的一个大坑就是第一点, 这个坑叫作忘记转换类型。
numbers = [1, 2, 3, 4, 5] squared = map(lambda n: n ** 2, numbers) print(squared) #正确做法:
squared = list(map(lambda n: n ** 2, numbers)) print(squared) # [1, 4, 9, 16, 25]这是第二个大坑, 就是你把参数的个数给写错了。
list1 = [1, 2, 3] list2 = [10, 20, 30] # 错误:lambda需要两个参数,但只给了一个 result = list(map(lambda x: x + y, list1, list2))正确做法:
result = list(map(lambda x, y: x + y, list1, list2))坑3:()忘记导入
numbers = [1, 2, 3, 4, 5] product = reduce(lambda x, y: x * y, numbers) # NameError: name 'reduce' is not defined正确做法:
from functools import reduce product = reduce(lambda x, y: x * y, numbers)实战练习
练习1: 把一个字符串列表里所有字符串变成大写,然后筛选出长度大于5的。
进行练习二的这个过程, 也就是要通过使用括号,这个操作来完成将1一直到100的范围之内, 所有的那些是奇数的数字去求和。
这是一个学生名单的练习任务, 我们需要执行的操作是先筛选出那些他们的平均考分数值大于八十的成绩记录, 接着对这些通过筛选的数据内容按照平均分的顺序从高到低进行排列处理。
答案已经发布到了评论区的部分, 希望大家先凭借自己的力量尝试解答。
互动时间
在你看来, 你是更倾向于去使用那个叫作map的函数, 还是说你更喜欢用列表推导式这种方式。
我之前刚开始学习这个知识点的时候, 心里觉得那个叫做map的东西特别酷, 在各种地方都在使用这个功能, 但是后来我发现列表推导式这种方法看上去更加直观, 我就主要去用列表推导式了, 然而在处理那些比较庞大的数据的时候, 我心里还是能够联想到map所具备的那种惰性求值方面的特性。
如果你在使用过程中有什么心得体验, 或者说存在什么疑问的话, 不妨到评论区里去交流一下吧。
下一节课要预告的内容是关于第六节第七小节, 也就是关于装饰器的话题, 这个说法的意思就像是给函数穿上一件外套。
我是一个从测试工作转行过来的、一个很普通的开发者。如果读者朋友们觉得这篇内容对自己确实有帮助的话, 可以点赞并且收藏起来, 而你们的这种支持行为, 就是我继续进行分享工作的动力的重要来源所在。