Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
前面我们学习了 Python 的函数参数传递,了解了变量、对象与引用之间的关系,也知道了如何通过函数封装重复执行的操作。
但在实际开发中,我们经常会遇到另一个问题:如果有一个包含几百个元素的列表,需要对每个元素执行相同的操作,应该怎么办?
比如,我们需要批量处理服务器的 CPU 使用率、提取日志中的错误信息,或者从一组监控数据中筛选出异常值。难道每次都要手动编写循环,再逐个处理每个元素吗?
当然不用。Python 提供了两个非常实用的内置函数:
map()和filter()。
map()负责将函数应用到可迭代对象的元素上,filter()则负责根据条件筛选元素。掌握这两个函数,可以让我们用更简洁的方式处理列表、元组等可迭代对象。不过,在学习它们之前,还要记住一个重要特点:
map()和filter()返回的都是迭代器,它们采用惰性执行的方式,不会在创建时立即处理所有元素。
为什么需要 map() 和 filter()?
先来看一个简单的需求。
假设我们获取了五台服务器的 CPU 使用率:
1 | cpu_usages = [35, 68, 92, 45, 87] |
现在有两个任务:
- 将每台服务器的 CPU 使用率转换成带有百分号的字符串。
- 找出 CPU 使用率超过 80% 的服务器。
如果使用熟悉的 for 循环,可以这样实现:
1 | cpu_usages = [35, 68, 92, 45, 87] |
运行结果:
1 | ['35%', '68%', '92%', '45%', '87%'] |
这段代码没有任何问题,而且在很多情况下,使用 for 循环依然是非常清晰、实用的选择。
不过,如果只是想把一个函数应用到每个元素上,或者根据条件筛选元素,我们还可以使用 map() 和 filter()。
先记住它们的基本区别:
| 函数 | 主要作用 | 可以理解为 |
|---|---|---|
map() | 对每个元素应用一个函数 | 批量转换 |
filter() | 根据条件保留符合要求的元素 | 批量筛选 |
接下来,我们分别学习这两个函数。
map():将函数应用到每个元素
map() 是 Python 的内置函数,它可以将指定函数应用到可迭代对象的每个元素上,并返回一个迭代器。
基本语法如下:
1 | map(function, iterable) |
其中:
function:要应用到元素上的函数。iterable:可迭代对象,例如列表、元组等。
例如,我们有一个整数列表:
1 | nums = [1, 2, 3, 4, 5] |
现在,希望计算每个数字的平方。
如果使用 for 循环,可以这样写:
1 | nums = [1, 2, 3, 4, 5] |
运行结果:
1 | [1, 4, 9, 16, 25] |
使用 map(),可以把同样的操作写得更加简洁:
1 | nums = [1, 2, 3, 4, 5] |
运行结果:
1 | [1, 4, 9, 16, 25] |
这里出现了一个新面孔:lambda。
暂时不用着急深入研究它。现在只需要知道:
1 | lambda x: x * x |
表示接收一个参数 x,并返回 x * x 的结果。
因此:
1 | map(lambda x: x * x, nums) |
就相当于将计算平方的操作依次应用到 nums 中的每个元素。
可以用下面的流程图理解整个过程:
flowchart TD
A["原始列表 [1, 2, 3, 4, 5]"] --> B["map() 创建迭代器"]
B --> C["逐个取出元素"]
C --> D["执行 x * x"]
D --> E["产生计算结果"]
E --> F["list() 消费迭代器"]
F --> G["[1, 4, 9, 16, 25]"]需要注意,map() 本身返回的是迭代器,而不是列表。
如果希望直接获得列表,可以使用:
1 | list(map(lambda x: x * x, nums)) |
这也是为什么前面的例子使用了 list()。
map() 不一定要配合 lambda
如果要应用的操作已经封装成一个普通函数,就没有必要专门再写一个 lambda。
例如,我们可以定义一个计算平方的函数:
1 | def square(x): |
运行结果:
1 | [1, 4, 9, 16, 25] |
这里的 map(square, nums) 会依次调用 square(),将列表中的每个元素作为参数传入。
需要注意,传给 map() 的是函数对象 square,而不是函数调用 square()。
简单来说:
square:表示函数本身。square(2):表示调用函数,并获取计算结果。
如果写成 map(square(), nums),就会因为调用 square() 时没有提供必需的参数而报错。
map() 的实际应用:批量格式化服务器监控数据
在 Linux 运维工作中,我们经常需要处理监控数据。
假设通过某个监控接口获取到了多台服务器的内存使用率:
1 | memory_usages = [42, 76, 89, 55, 91] |
如果需要将它们转换成便于展示的字符串,可以这样写:
1 | memory_usages = [42, 76, 89, 55, 91] |
运行结果:
1 | ['42%', '76%', '89%', '55%', '91%'] |
这里,map() 将格式化操作应用到了每一个元素上。
如果以后监控数据从五个元素变成五百个元素,也不需要针对每个元素单独编写处理代码。
当然,map() 并不会自动让程序并行执行,也不意味着它一定比 for 循环更快。它的主要价值是提供一种简洁的函数式数据处理方式。
filter():根据条件筛选元素
了解了 map() 之后,再来看 filter()。
如果说 map() 关注的是如何转换每个元素,那么 filter() 关注的就是:哪些元素应该保留下来?
基本语法如下:
1 | filter(function, iterable) |
其中:
function:用于判断元素是否符合条件的函数。iterable:需要筛选的可迭代对象。
当提供判断函数时,filter() 会根据函数返回值的真假决定是否保留元素:
- 返回真值:保留当前元素。
- 返回假值:过滤掉当前元素。
例如,我们有一个整数列表:
1 | nums = [1, 2, 3, 4, 5, 6] |
现在希望筛选出所有偶数。
使用 for 循环,可以这样写:
1 | nums = [1, 2, 3, 4, 5, 6] |
运行结果:
1 | [2, 4, 6] |
使用 filter(),可以写成:
1 | nums = [1, 2, 3, 4, 5, 6] |
运行结果:
1 | [2, 4, 6] |
这里:
1 | lambda x: x % 2 == 0 |
表示判断 x 除以 2 的余数是否为 0。
如果条件成立,表达式返回 True,filter() 就保留这个元素;否则返回 False,这个元素就会被过滤掉。
可以用下面的流程图理解:
flowchart TD
A["原始列表 [1, 2, 3, 4, 5, 6]"] --> B["filter() 创建迭代器"]
B --> C["逐个取出元素并判断"]
C --> D{"满足偶数条件?"}
D -->|是| E["保留元素"]
D -->|否| F["过滤元素"]
E --> G["list() 消费迭代器"]
F --> C
G --> H["[2, 4, 6]"]与 map() 类似,filter() 返回的也是迭代器。如果希望得到列表,需要使用 list() 进行转换。
filter() 也可以使用普通函数
如果筛选条件比较复杂,可以把条件单独封装成函数。
例如,我们需要找出 CPU 使用率超过 80% 的服务器:
1 | def is_high_cpu(usage): |
运行结果:
1 | [92, 87] |
这里,filter() 会将每个 CPU 使用率传给 is_high_cpu(),根据函数返回的结果决定是否保留该元素。
如果后续筛选条件发生变化,例如将阈值调整为 90%,只需要修改判断函数即可:
1 | def is_high_cpu(usage): |
这种方式特别适合条件比较复杂,或者同一判断逻辑需要重复使用的场景。
filter() 的特殊用法:过滤空值
filter() 还有一种比较特别的用法:如果第一个参数是 None,它会根据元素自身的真假值进行筛选。
例如:
1 | values = [0, 1, "", "Linux", None, [], [1, 2], False] |
运行结果:
1 | [1, 'Linux', [1, 2]] |
这里的 None 不是表示“筛选出值为 None 的元素”,而是告诉 filter():不需要调用指定的判断函数,直接判断每个元素自身的真假值。
在 Python 中,以下值通常会被视为假值:
- 数值零,例如
0、0.0 FalseNone- 空字符串
"" - 空列表
[] - 空元组
() - 空字典
{} - 空集合
set()
因此,执行 filter(None, values) 时,这些假值都会被过滤掉,剩下的是真值元素。
这个用法在清理简单的数据列表时很方便。例如:
1 | server_names = ["web-01", "", "web-02", None, "web-03", ""] |
运行结果:
1 | ['web-01', 'web-02', 'web-03'] |
不过,这种写法也有一个需要特别注意的地方:它会同时过滤数值 0 和其他假值,并不只是过滤 None 或空字符串。
假设我们正在处理服务器的重试次数:
1 | retry_counts = [0, 1, 0, 3, None, 5] |
运行结果:
1 | [1, 3, 5] |
这里的 0 可能是一个有意义的值,例如表示某台服务器尚未发生重试。如果直接使用 filter(None, retry_counts),就会把这些零也过滤掉。
如果只想排除 None,可以明确写出判断条件:
1 | retry_counts = [0, 1, 0, 3, None, 5] |
运行结果:
1 | [0, 1, 0, 3, 5] |
如果只想过滤空字符串,也应该明确指定条件:
1 | values = ["Linux", "", "Python", "", "Docker"] |
运行结果:
1 | ['Linux', 'Python', 'Docker'] |
所以,使用 filter(None, iterable) 之前,一定要确认:你是真的想过滤所有假值,还是只想排除某一种特定的空值。
filter() 的实际应用:筛选异常服务器
假设我们从监控系统中获取了一组服务器信息:
1 | servers = [ |
现在希望找出 CPU 使用率超过 80% 的服务器。
可以使用 filter():
1 | servers = [ |
运行结果:
1 | [{'name': 'web-02', 'cpu': 92}, {'name': 'web-04', 'cpu': 87}] |
这里,filter() 并不是只能处理整数。只要判断函数能够处理相应的元素,就可以筛选字典、字符串,或者其他可迭代对象中的数据。
这对于处理监控数据、日志解析结果以及 API 返回的结构化数据都很有帮助。
map() 和 filter() 的惰性执行
前面我们一直在使用:
1 | list(map(...)) |
这很容易让人产生一个误解:是不是调用 map() 或 filter() 时,Python 就立即处理完了所有元素?
答案是:不是。map() 和 filter() 都是惰性迭代器,创建它们时并不会立即处理所有输入元素。
所谓惰性执行,可以简单理解为:先准备好处理规则,需要获取结果时,再逐步执行相应的操作。
我们先通过一个例子观察 map()。
观察 map() 什么时候执行函数
1 | def square(x): |
运行结果:
1 | 创建 map 迭代器 |
仔细观察输出顺序。
当执行:
1 | result = map(square, nums) |
程序并没有立即打印每个数字的平方,而是先创建了一个迭代器。
直到执行:
1 | next(result) |
程序才开始处理第一个元素,调用 square(1),得到结果 1。
再次执行 next(result),才继续处理第二个元素。
这说明,map() 并不是创建时就把所有结果计算好,而是在需要获取元素时才调用对应的函数。
整个过程可以表示为:
flowchart TD
A["调用 map(square, nums)"] --> B["创建 map 迭代器"]
B --> C["暂不处理所有元素"]
C --> D["调用 next() 获取元素"]
D --> E["执行 square()"]
E --> F["返回计算结果"]
F --> G{"还需要下一个元素吗?"}
G -->|是| D
G -->|否| H["停止消费迭代器"]这里的 next() 是 Python 的内置函数,用于从迭代器中获取下一个元素。
如果迭代器中已经没有剩余元素,再调用 next() 就会抛出 StopIteration 异常。日常使用 for 循环时,Python 会自动处理这种结束情况。
filter() 也是惰性执行
filter() 的执行方式也类似,不过它还有一个特点:为了找到下一个符合条件的元素,可能需要连续检查多个输入元素。
来看一个例子:
1 | def is_even(x): |
运行结果:
1 | 创建 filter 迭代器 |
为什么第一次调用 next(result) 时,判断函数执行了两次?
因为第一个元素 1 不符合条件,filter() 会继续检查 2,直到找到符合条件的元素,才将它返回。
第二次调用 next(result) 时,也是同样的道理:先检查 3,再检查 4,最终返回 4。
这说明,filter() 的惰性执行并不意味着每次调用 next() 只检查一个元素,而是会一直检查输入数据,直到找到符合条件的元素,或者确认输入已经耗尽。
为什么惰性执行很有用?
假设有一个包含一百万个数字的可迭代对象,我们只想取出前面几个处理结果。
使用 map(),可以这样写:
1 | def square(x): |
运行结果:
1 | 计算 1 的平方 |
虽然输入范围包含一百万个数字,但程序只计算了前三个元素。
如果一开始就把所有结果转换成列表:
1 | result = list(map(square, range(1, 1000001))) |
那么程序就会尝试处理全部一百万个元素,并将结果保存到列表中。
因此,惰性执行可以让我们按需处理数据,避免不必要的计算和一次性保存全部结果。
不过,也要记住两个细节:
map()和filter()的惰性特征不代表所有操作都惰性执行。例如,list()会立即消费迭代器中的所有剩余元素。- 如果输入本身已经是列表,那么列表的数据早已在内存中。使用
map()并不会让这个原始列表变成惰性的,只是转换结果会按需产生。
对于大型数据集、数据处理流水线等场景,惰性迭代非常实用。但如果最终仍然需要把所有结果放进列表,就仍然需要相应的内存空间。
map() 和 filter() 有什么区别?
学习到这里,我们可以把两者放在一起比较。
| 对比项目 | map() | filter() |
|---|---|---|
| 主要作用 | 转换元素 | 筛选元素 |
| 函数返回值的用途 | 作为转换后的结果 | 判断是否保留元素 |
| 输出元素数量 | 与输入元素数量相同(单个可迭代对象的常见用法) | 不会超过输入元素数量 |
| 返回类型 | 迭代器 | 迭代器 |
| 是否惰性执行 | 是 | 是 |
| 常见用途 | 格式化、计算、转换 | 条件筛选、异常数据过滤 |
例如:
1 | nums = [1, 2, 3, 4, 5] |
运行结果:
1 | [1, 4, 9, 16, 25] |
map() 将每个数字转换成它的平方,filter() 则只保留偶数。
用一句话概括:
需要把每个元素转换成另一个结果时,考虑 map();需要根据条件保留部分元素时,考虑 filter()。
map() 和 filter() 可以组合使用吗?
当然可以。
在实际开发中,我们经常需要先筛选数据,再对筛选结果进行转换。
例如,找出 CPU 使用率超过 80% 的服务器,再将它们格式化成带有百分号的字符串。
1 | cpu_usages = [35, 68, 92, 45, 87] |
运行结果:
1 | ['92%', '87%'] |
这个过程可以理解为:
flowchart LR
A["原始 CPU 数据"] --> B["filter() 筛选"]
B --> C["保留超过 80% 的数据"]
C --> D["map() 格式化"]
D --> E["['92%', '87%']"]这里没有必要先将 filter() 的结果转换成列表,再传给 map()。因为 filter() 返回的迭代器本身就是可迭代对象,可以直接作为 map() 的输入。
不过,迭代器通常只能从当前位置继续向前消费。遍历或转换成列表之后,如果还想重新处理完整的数据,通常需要重新创建迭代器,或者事先保存结果。
代码并不是越短越好。如果处理逻辑比较简单,使用 for 循环往往更直观;如果转换和筛选逻辑复杂,也可以将步骤拆开,让代码更容易阅读和维护。
除了 map() 和 filter(),还有其他选择吗?
有。
Python 中,列表推导式往往是处理列表转换和筛选时非常常用的写法。
例如,计算每个数字的平方:
1 | nums = [1, 2, 3, 4, 5] |
运行结果:
1 | [1, 4, 9, 16, 25] |
筛选偶数:
1 | nums = [1, 2, 3, 4, 5, 6] |
运行结果:
1 | [2, 4, 6] |
它们分别对应使用 map() 进行转换,以及使用 filter() 进行筛选的常见场景。
那么,应该选择哪种写法?
- 列表推导式:转换或筛选逻辑简单时,通常简洁直观。
map():已经有一个适合复用的转换函数时,可以直接应用它。filter():已经有一个适合复用的判断函数时,可以直接应用它。for循环:需要多个步骤、复杂分支或副作用时,往往更清晰。
还有一点需要注意:列表推导式会立即生成列表,而 map() 和 filter() 返回迭代器。这意味着,如果只需要处理前几个结果,迭代器通常可以避免提前计算所有元素。
这些方式并不存在绝对的优劣。实际开发时,优先选择容易理解、方便维护的写法。
写在最后
这篇文章我们学习了两个非常实用的 Python 内置函数:map() 和 filter()。
最后,记住几个核心知识点:
map()将函数应用到可迭代对象的元素上,常用于批量转换。filter()根据判断函数的返回值筛选元素。filter(None, iterable)会保留真值元素、过滤假值元素,使用时要注意0、空字符串和None的区别。map()和filter()都返回迭代器,采用惰性执行方式,需要获取结果时才逐步处理元素。- 使用
list()会立即消费迭代器,并将结果保存到列表中。 - 列表推导式和
for循环也是常见的替代方案,应根据实际场景选择。
对于 Linux 运维人员来说,这些工具可以帮助我们处理服务器监控数据、过滤异常指标,以及转换 API 返回的数据。不过,map() 和 filter() 本身并不负责连接服务器、调用监控接口或并行执行任务,它们主要负责处理已经获取到的可迭代数据。
下一篇,我们将继续学习 嵌套函数与 lambda 表达式。
本文已经多次使用了 lambda,例如:
1 | lambda x: x * x |
我们知道它可以用来编写简单的函数,但它和普通的 def 函数究竟有什么区别?为什么有些地方适合使用 lambda,有些地方却更适合定义普通函数?嵌套函数又是怎么回事?
下一篇,我们就从这些问题出发,继续深入理解 Python 的函数。
