map 函数的队友与对手
上篇我们一起学习了 Python 中的内置函数 map() ,其可以在不使用显式 for 循环的情况下处理和转换一个迭代器中的所有项目。 map() 是支持 Python 中函数式编程风格的工具之一。
本文将学习如何将 map() 与其他函数工具 结合起来 ,并进行更复杂的转换; 并学习可以用什么工具来 替代map() ,使代码更加 Pythonic 。
map() 与其他函数结合使用
现在我们已经介绍了如何使用
map()
来完成涉及迭代表的不同任务。然而,如果将
map()
与其他函数式工具(如
filter()
和
reduce()
)一起使用,那么我们可以对迭代变量进行更复杂的转换。这就是在下面要介绍的内容。
map() 和 filter()
有时需要处理一个输入迭代器,并返回另一个迭代器,这个迭代器是过滤掉输入迭代器中不需要的值而得到的。在这种情况下,Python 的
filter()
可以是一个很好的选择。
filter()
是一个内置的函数,需要两个位置参数。
-
function函数将是一个谓词或布尔值函数,一个根据输入数据返回真或假的函数。 -
iterable是任何Python可迭代对象。
filter()
产生的是函数返回 True 的输入迭代器的项目。如果把 None传给函数,那么
filter()
使用身份函数。这意味着
filter()
将检查可迭代对象中每个项目的真值,并过滤掉所有虚假的项目。
为了说明如何使用
map()
和
filter()
,假设我们需要计算一个列表中所有数值的平方根,如果列表中包含负值时,会得到一个错误。
>>> import math>>> math.sqrt(-16)Traceback (most recent call last): File "<input>", line 1, in <module> math.sqrt(-16)ValueError: math domain error
如果有一个负数作为参数,
math.sqrt()
会引发一个ValueError。为了避免这个问题,可以使用
filter()
来过滤掉所有的负值,然后找到剩余正值的平方根。看下面的例子。
>>> import math>>> def is_positive(num):... return num >= 0...>>> def sanitized_sqrt(numbers):... cleaned_iter = map(math.sqrt, filter(is_positive, numbers))... return list(cleaned_iter)...>>> sanitized_sqrt([25, 9, 81, -16, 0])[5.0, 3.0, 9.0, 0.0]
is_positive()
是一个谓词函数,它接受一个数字作为参数,如果该数字大于或等于0,则返回True。可以将
is_positive()
传递给
filter()
,以去除数字中的所有负数。因此对
map()
的调用将只处理正数,从而确保
math.sqrt()
不会抛出一个ValueError。
map() 和 reduce()
reduce()
是一个函数,它存在于 Python 标准库中一个叫做
functools
的模块中。
reduce()
是 Python 的另一个核心函数工具,当我们需要将一个函数应用于一个迭代器并将其减少到一个单一的累积值时,它非常有用,这种操作通常被称为
减少或折叠
。
reduce()
需要两个必要的参数。
-
function函数可以是任何接受两个参数并返回一个值的 Python 可调用函数。 -
iterable 可以是任何 Python 的可迭代对象。
reduce()
将对可迭代对象
iterable
中的所有项目应用函数,并累积计算出一个最终值。
下面的例子结合了
map()
和
reduce()
来计算主目录中所有文件的总大小:
>>> import functools>>> import operator>>> import os>>> import os.path>>> files = os.listdir(os.path.expanduser("~"))>>> functools.reduce(operator.add, map(os.path.getsize, files))4377381
在这个例子中,我们调用
os.path.expanduser("~")
来获得主目录的路径。然后在该路径上调用
os.listdir()
,得到一个包含所有文件路径的列表。
对
map()
的调用使用
os.path.getsize()
来获得每个文件的大小。最后使用
reduce()
和
operator.add()
来获得每个文件大小的累积总和。最后的结果是主目录中所有文件的总大小,单位是字节。
注意 :几年前,谷歌开发并开始使用他们称之为
MapReduce的编程模型。这是一种新的数据处理方式,旨在使用集群上的并行和分布式计算来管理大数据。
这个模型的灵感来自于函数式编程中常用的 map 和 reduce 操作的结合。
MapReduce模型对谷歌在合理时间内处理海量数据的能力产生了巨大影响。然而,到了2014年,谷歌不再使用MapReduce作为他们的主要处理模式。
现在,我们可以找到一些MapReduce的替代实现,如 Apache Hadoop,它是一个使用MapReduce模型的开源软件工具的集合。
尽管可以使用
reduce()
来解决本节所涉及的问题,但 Python 提供了其他的工具,这些工具可以导致一个更加 Pythonic 和高效的解决方案。例如可以使用内置的函数
sum()
来计算主目录中的文件的总大小。
>>> import os>>> import os.path>>> files = os.listdir(os.path.expanduser("~"))>>> sum(map(os.path.getsize, files))4377381
这个例子比我们之前看到的例子可读性和效率都要高很多。
用 starmap() 处理基于元组的可迭代对象
Python的
itertools.starmap()
生成一个迭代器,该迭代器将函数应用于从元组可迭代对象获得的参数,并产生结果。当处理已经分组在元组中的可迭代对象时,它很有用。
map()
和
starmap()
之间的主要区别在于后者使用解包操作符(
*
)调用其转换函数,将每个元组参数解包为几个位置参数。因此,转换函数被称为
function(*args)
而不是
function(arg1, arg2,... argN)
。
starmap()的官方文档 [1] 说,该函数大致等同于下面的Python函数。
def starmap(function, iterable): for args in iterable: yield function(*args)
这个函数中的for循环对 itera ble 中的项目进行迭代,并产生转换后的项目作为结果。对 function(*args) 的调用使用了解包操作符,将图元解包为几个位置参数。下面是一些关于 starmap() 如何工作的例子。
这个函数中的
for
循环遍历
iterable
中的元素,并得到转换后的结果。调用
function(*args)
使用解包操作符将元组解包为几个位置参数。下面是一些关于
starmap()
如何工作的例子:
>>> from itertools import starmap>>> list(starmap(pow, [(2, 7), (4, 3)]))[128, 64]>>> list(starmap(ord, [(2, 7), (4, 3)]))Traceback (most recent call last): File "<input>", line 1, in <module> list(starmap(ord, [(2, 7), (4, 3)]))TypeError: ord() takes exactly one argument (2 given)
在第一个例子中,使用
pow()
来计算每个元组中第一个值对第二个值的升幂。这些元组的形式是
(基数, 指数)
。
如果可迭代对象中的每个元组都有两个元素,那么
function
也必须接受两个参数。如果元组有三个元素,那么
function
必须接受三个参数,依此类推。否则会得到一个
TypeError
如果使用
map()
而不是
starmap()
,那么会得到一个不同的结果,因为
map()
从每个元组中抽取一个项目。
>>> list(map(pow, (2, 7), (4, 3)))[16, 343]
注意 ,
map()需要两个元组,而不是一个元组的列表。map()在每次迭代中也从每个元组中获取一个值。要使map()返回与starmap()相同的结果,需要交换值。
>>> list(map(pow, (2, 4), (7, 3)))[128, 64]
在这种情况下,我们有两个元组而不是一个元组的列表,还交换了7和4。现在,第一个元组提供基数,第二个元组提供指数。
用Pythonic风格编码取代map()
像
map()
、
filter()
和
reduce()
这样的函数式编程工具已经存在很长时间了。然而,
列表推导式
和
生成器表达式
几乎在每个用例中都成为了它们的自然替代品。
例如,
map()
提供的功能几乎总是用一个
列表推导式
或
生成器表达式
来表达更好。在下面两节中,我们将学习如何用列表推导式或生成器表达式来替换对
map()
的调用,使我们的代码更有可读性和Pythonic。
使用列表推导式
有一个一般的模式,我们可以用一个列表推导式来代替对
map()
的调用。具体方法如下。
# 用map生成一个列表list(map(function, iterable))# 用list comprehension生成一个列表[function(x) for x in iterable]。
注意,列表推导几乎总是比调用
map()
读起来更清楚。由于列表推导式在Python开发人员中非常流行,所以在任何地方都可以找到它们。因此,用列表推导式替换
map()
调用会让其他Python开发人员更熟悉你的代码。
这里有一个例子,说明如何用一个列表推导式来代替
map()
,建立一个平方数的列表。
>>> # 变换函数>>> def square(number):... return number ** 2>>> numbers = [1, 2, 3, 4, 5, 6]>>> # 使用map()>>> list(map(square, numbers))[1, 4, 9, 16, 25, 36]>>> # 使用列表理解>>> [square(x) for x in numbers][1, 4, 9, 16, 25, 36]
如果我们比较这两种解决方案,那么我们可能会说,使用列表理解的那个方案更有可读性,因为它读起来几乎像纯英语。另外,列表理解避免了在 map() 上明确调用 list() 来建立最终的列表。
使用生成器表达式
map()
返回一个
map对象
,它是一个迭代器,可以按需产生项目。因此,
map()
的自然替代物是一个
生成器表达式
[2]
,因为生成器表达式返回生成器对象,而生成器对象也是按需产生项目的迭代器。
map()
返回一个
map对象
,这是一个按需生成项目的迭代器。因此对
map()
的自然替换是一个生成器表达式,因为生成器表达式返回生成器对象,这些对象也是生成按需项的迭代器。
众所周知,Python迭代器在内存消耗方面是非常高效的。这就是为什么 map() 现在返回一个迭代器而不是一个列表的原因。
列表推导式和生成器表达式之间有一个微小的语法差异。第一种方法使用一对方括号(
'[]'
)来分隔表达式。第二个使用一对圆括号(
'()'
)。因此,要将列表推导式转换为生成器表达式,只需将方括号替换为圆括号。
可以使用生成器表达式来编写代码,它比使用
map()
的代码读起来更清晰。请看下面的例子。
>>> # 变换函数>>> def square(number):... return number ** 2>>> numbers = [1, 2, 3, 4, 5, 6]>>> # 使用map()>>> map_obj = map(square, numbers)>>> map_obj<map object at 0x7f254d180a60>>>> list(map_obj)[1, 4, 9, 16, 25, 36]>>> # 使用一个生成器表达式>>> gen_exp = (square(x) for x in numbers)>>> gen_exp<generator object <genexpr> at 0x7f254e056890>>>> list(gen_exp)[1, 4, 9, 16, 25, 36]
这段代码与上一节的代码有一个主要区别:把方括号改为一对小括号,把列表理解变成生成器表达式。
生成器表达式通常用作函数调用中的参数。在这种情况下,不需要使用圆括号来创建生成器表达式,因为用于调用函数的圆括号还提供了构建生成器的语法。有了这个想法,通过像这样调用
list()
,你可以得到与上面例子相同的结果:
>>> list(square(x) for x in numbers)[1, 4, 9, 16, 25, 36]
如果在函数调用中使用生成器表达式作为参数,那么就不需要额外的一对小括号。我们用来调用函数的小括号提供了构建生成器的语法。
在内存消耗方面,生成器表达式与
map()
一样高效,因为它们都返回按需生成项的迭代器。然而,生成器表达式几乎总是会提高代码的可读性。它们还使您的代码在其他Python开发人员眼中更像Python。
总结
我们可以使用Python的
map()
对可迭代对象执行映射操作。映射操作包括对可迭代对象中的元素应用转换函数来生成转换后的可迭代对象。通常,
map()
可以处理和转换可迭代对象,而无需使用显式循环。
在本文中,我们已经学习了
map()
如何工作以及如何使用它来处理可迭代对象。还了解了一些可以在代码中替换
map()
的python工具。
至此我们现在知道如何:
-
使用Python的 map() -
使用 map()来 处理 和 转换 可迭代对象,而不使用显式循环 -
将 map()与filter()和reduce()等函数组合在一起,以执行复杂的转换 -
用 列表推导式 和 生成器表达式 等工具替换 map()
有了这些新知识,将能够在代码中使用
map()
,并以函数式编程风格处理代码。通过将
map()
替换为列表推导式或生成器表达式,还可以切换到更python化和更现代的风格。
参考资料
starmap的官方文档: https://docs.python.org/3/library/itertools.html#itertools.starmap
[2]生成器表达式: https://realpython.com/introduction-to-python-generators/#building-generators-with-generator-expressions
🏴☠️宝藏级🏴☠️ 原创公众号『 数据STUDIO 』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括 可戳 👉 Python | MySQL | 数据分析 | 数据可视化 | 机器学习与数据挖掘 | 爬虫 等,从入门到进阶!
长按👇关注- 数据STUDIO -设为星标,干货速递