1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpu_usages = [35, 68, 92, 45, 87]

formatted_usages = []

for usage in cpu_usages:
formatted_usages.append(f"{usage}%")

print(formatted_usages)

high_usages = []

for usage in cpu_usages:
if usage > 80:
high_usages.append(usage)

print(high_usages)

运行结果:

1
2
['35%', '68%', '92%', '45%', '87%']
[92, 87]

这段代码没有任何问题,而且在很多情况下,使用 for 循环依然是非常清晰、实用的选择。

不过,如果只是想把一个函数应用到每个元素上,或者根据条件筛选元素,我们还可以使用 map() 和 filter()。

先记住它们的基本区别:

函数主要作用可以理解为
map()对每个元素应用一个函数批量转换
filter()根据条件保留符合要求的元素批量筛选

接下来,我们分别学习这两个函数。

map():将函数应用到每个元素

map() 是 Python 的内置函数,它可以将指定函数应用到可迭代对象的每个元素上,并返回一个迭代器。

基本语法如下:

1
map(function, iterable)

其中:

  • function:要应用到元素上的函数。
  • iterable:可迭代对象,例如列表、元组等。

例如,我们有一个整数列表:

1
nums = [1, 2, 3, 4, 5]

现在,希望计算每个数字的平方。

如果使用 for 循环,可以这样写:

1
2
3
4
5
6
7
8
nums = [1, 2, 3, 4, 5]

squared = []

for num in nums:
squared.append(num * num)

print(squared)

运行结果:

1
[1, 4, 9, 16, 25]

使用 map(),可以把同样的操作写得更加简洁:

1
2
3
4
5
nums = [1, 2, 3, 4, 5]

squared = list(map(lambda x: x * x, nums))

print(squared)

运行结果:

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
2
3
4
5
6
7
8
9
def square(x):
return x * x


nums = [1, 2, 3, 4, 5]

squared = list(map(square, nums))

print(squared)

运行结果:

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
2
3
4
5
memory_usages = [42, 76, 89, 55, 91]

formatted = list(map(lambda usage: f"{usage}%", memory_usages))

print(formatted)

运行结果:

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
2
3
4
5
6
7
8
9
nums = [1, 2, 3, 4, 5, 6]

evens = []

for num in nums:
if num % 2 == 0:
evens.append(num)

print(evens)

运行结果:

1
[2, 4, 6]

使用 filter(),可以写成:

1
2
3
4
5
nums = [1, 2, 3, 4, 5, 6]

evens = list(filter(lambda x: x % 2 == 0, nums))

print(evens)

运行结果:

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
2
3
4
5
6
7
8
9
def is_high_cpu(usage):
return usage > 80


cpu_usages = [35, 68, 92, 45, 87]

high_usages = list(filter(is_high_cpu, cpu_usages))

print(high_usages)

运行结果:

1
[92, 87]

这里,filter() 会将每个 CPU 使用率传给 is_high_cpu(),根据函数返回的结果决定是否保留该元素。

如果后续筛选条件发生变化,例如将阈值调整为 90%,只需要修改判断函数即可:

1
2
def is_high_cpu(usage):
return usage > 90

这种方式特别适合条件比较复杂,或者同一判断逻辑需要重复使用的场景。

filter() 的特殊用法:过滤空值

filter() 还有一种比较特别的用法:如果第一个参数是 None,它会根据元素自身的真假值进行筛选。

例如:

1
2
3
4
5
values = [0, 1, "", "Linux", None, [], [1, 2], False]

result = list(filter(None, values))

print(result)

运行结果:

1
[1, 'Linux', [1, 2]]

这里的 None 不是表示“筛选出值为 None 的元素”,而是告诉 filter():不需要调用指定的判断函数,直接判断每个元素自身的真假值。

在 Python 中,以下值通常会被视为假值:

  • 数值零,例如 0、0.0
  • False
  • None
  • 空字符串 ""
  • 空列表 []
  • 空元组 ()
  • 空字典 {}
  • 空集合 set()

因此,执行 filter(None, values) 时,这些假值都会被过滤掉,剩下的是真值元素。

这个用法在清理简单的数据列表时很方便。例如:

1
2
3
4
5
server_names = ["web-01", "", "web-02", None, "web-03", ""]

valid_names = list(filter(None, server_names))

print(valid_names)

运行结果:

1
['web-01', 'web-02', 'web-03']

不过,这种写法也有一个需要特别注意的地方:它会同时过滤数值 0 和其他假值,并不只是过滤 None 或空字符串。

假设我们正在处理服务器的重试次数:

1
2
3
4
5
retry_counts = [0, 1, 0, 3, None, 5]

result = list(filter(None, retry_counts))

print(result)

运行结果:

1
[1, 3, 5]

这里的 0 可能是一个有意义的值,例如表示某台服务器尚未发生重试。如果直接使用 filter(None, retry_counts),就会把这些零也过滤掉。

如果只想排除 None,可以明确写出判断条件:

1
2
3
4
5
retry_counts = [0, 1, 0, 3, None, 5]

result = list(filter(lambda x: x is not None, retry_counts))

print(result)

运行结果:

1
[0, 1, 0, 3, 5]

如果只想过滤空字符串,也应该明确指定条件:

1
2
3
4
5
values = ["Linux", "", "Python", "", "Docker"]

result = list(filter(lambda x: x != "", values))

print(result)

运行结果:

1
['Linux', 'Python', 'Docker']

所以,使用 filter(None, iterable) 之前,一定要确认:你是真的想过滤所有假值,还是只想排除某一种特定的空值。

filter() 的实际应用:筛选异常服务器

假设我们从监控系统中获取了一组服务器信息:

1
2
3
4
5
6
servers = [
{"name": "web-01", "cpu": 35},
{"name": "web-02", "cpu": 92},
{"name": "web-03", "cpu": 68},
{"name": "web-04", "cpu": 87}
]

现在希望找出 CPU 使用率超过 80% 的服务器。

可以使用 filter():

1
2
3
4
5
6
7
8
9
10
11
12
servers = [
{"name": "web-01", "cpu": 35},
{"name": "web-02", "cpu": 92},
{"name": "web-03", "cpu": 68},
{"name": "web-04", "cpu": 87}
]

high_cpu_servers = list(
filter(lambda server: server["cpu"] > 80, servers)
)

print(high_cpu_servers)

运行结果:

1
[{'name': 'web-02', 'cpu': 92}, {'name': 'web-04', 'cpu': 87}]

这里,filter() 并不是只能处理整数。只要判断函数能够处理相应的元素,就可以筛选字典、字符串,或者其他可迭代对象中的数据。

这对于处理监控数据、日志解析结果以及 API 返回的结构化数据都很有帮助。

map() 和 filter() 的惰性执行

前面我们一直在使用:

1
2
list(map(...))
list(filter(...))

这很容易让人产生一个误解:是不是调用 map() 或 filter() 时,Python 就立即处理完了所有元素?

答案是:不是。map() 和 filter() 都是惰性迭代器,创建它们时并不会立即处理所有输入元素。

所谓惰性执行,可以简单理解为:先准备好处理规则,需要获取结果时,再逐步执行相应的操作。

我们先通过一个例子观察 map()。

观察 map() 什么时候执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def square(x):
print(f"正在计算 {x} 的平方")
return x * x


nums = [1, 2, 3]

print("创建 map 迭代器")
result = map(square, nums)

print("map 迭代器已经创建")

print(next(result))
print(next(result))

运行结果:

1
2
3
4
5
6
创建 map 迭代器
map 迭代器已经创建
正在计算 1 的平方
1
正在计算 2 的平方
4

仔细观察输出顺序。

当执行:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
def is_even(x):
print(f"正在判断 {x} 是否为偶数")
return x % 2 == 0


nums = [1, 2, 3, 4]

print("创建 filter 迭代器")
result = filter(is_even, nums)

print("filter 迭代器已经创建")

print(next(result))
print(next(result))

运行结果:

1
2
3
4
5
6
7
8
创建 filter 迭代器
filter 迭代器已经创建
正在判断 1 是否为偶数
正在判断 2 是否为偶数
2
正在判断 3 是否为偶数
正在判断 4 是否为偶数
4

为什么第一次调用 next(result) 时,判断函数执行了两次?

因为第一个元素 1 不符合条件,filter() 会继续检查 2,直到找到符合条件的元素,才将它返回。

第二次调用 next(result) 时,也是同样的道理:先检查 3,再检查 4,最终返回 4。

这说明,filter() 的惰性执行并不意味着每次调用 next() 只检查一个元素,而是会一直检查输入数据,直到找到符合条件的元素,或者确认输入已经耗尽。

为什么惰性执行很有用?

假设有一个包含一百万个数字的可迭代对象,我们只想取出前面几个处理结果。

使用 map(),可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
def square(x):
print(f"计算 {x} 的平方")
return x * x


result = map(square, range(1, 1000001))

first_three = []

for _ in range(3):
first_three.append(next(result))

print(first_three)

运行结果:

1
2
3
4
计算 1 的平方
计算 2 的平方
计算 3 的平方
[1, 4, 9]

虽然输入范围包含一百万个数字,但程序只计算了前三个元素。

如果一开始就把所有结果转换成列表:

1
result = list(map(square, range(1, 1000001)))

那么程序就会尝试处理全部一百万个元素,并将结果保存到列表中。

因此,惰性执行可以让我们按需处理数据,避免不必要的计算和一次性保存全部结果。

不过,也要记住两个细节:

  • map() 和 filter() 的惰性特征不代表所有操作都惰性执行。例如,list() 会立即消费迭代器中的所有剩余元素。
  • 如果输入本身已经是列表,那么列表的数据早已在内存中。使用 map() 并不会让这个原始列表变成惰性的,只是转换结果会按需产生。

对于大型数据集、数据处理流水线等场景,惰性迭代非常实用。但如果最终仍然需要把所有结果放进列表,就仍然需要相应的内存空间。

map() 和 filter() 有什么区别?

学习到这里,我们可以把两者放在一起比较。

对比项目map()filter()
主要作用转换元素筛选元素
函数返回值的用途作为转换后的结果判断是否保留元素
输出元素数量与输入元素数量相同(单个可迭代对象的常见用法)不会超过输入元素数量
返回类型迭代器迭代器
是否惰性执行是是
常见用途格式化、计算、转换条件筛选、异常数据过滤

例如:

1
2
3
4
5
6
7
nums = [1, 2, 3, 4, 5]

squared = list(map(lambda x: x * x, nums))
evens = list(filter(lambda x: x % 2 == 0, nums))

print(squared)
print(evens)

运行结果:

1
2
[1, 4, 9, 16, 25]
[2, 4]

map() 将每个数字转换成它的平方,filter() 则只保留偶数。

用一句话概括:

需要把每个元素转换成另一个结果时,考虑 map();需要根据条件保留部分元素时,考虑 filter()。

map() 和 filter() 可以组合使用吗?

当然可以。

在实际开发中,我们经常需要先筛选数据,再对筛选结果进行转换。

例如,找出 CPU 使用率超过 80% 的服务器,再将它们格式化成带有百分号的字符串。

1
2
3
4
5
6
cpu_usages = [35, 68, 92, 45, 87]

high_usages = filter(lambda usage: usage > 80, cpu_usages)
formatted = list(map(lambda usage: f"{usage}%", high_usages))

print(formatted)

运行结果:

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
2
3
4
5
nums = [1, 2, 3, 4, 5]

squared = [x * x for x in nums]

print(squared)

运行结果:

1
[1, 4, 9, 16, 25]

筛选偶数:

1
2
3
4
5
nums = [1, 2, 3, 4, 5, 6]

evens = [x for x in nums if x % 2 == 0]

print(evens)

运行结果:

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 的函数。