series: python-intro


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 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
  33. Python 系列(三十三) 什么是类和对象?零基础理解 Python class
  34. Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
  35. Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
  36. Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
  37. Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
  38. Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
  39. Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
  40. Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
  41. Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
  42. Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
  43. Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
  44. Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
  45. Python 系列(四十五) 使用 sys 模块处理标准输入与输出
  46. Python 系列(四十六) 使用 open() 读取和写入文件
  47. Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
  48. Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
  49. Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
  50. Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
  51. Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
  52. Python 系列(五十二) 掌握字典推导式,让字典创建更简单
  53. Python 系列(五十三) 从零理解生成器,轻松处理大量数据
  54. Python 系列(五十五) 使用 zip() 同时遍历多个集合

前面我们学习了生成器,知道可以使用 yield 让函数一次产生一个结果,而不是一次性返回所有结果。

例如,我们可以编写一个生成器函数,依次计算数字的平方:

1
2
3
4
5
6
7
def generate_squares():
for x in range(5):
yield x * x


for value in generate_squares():
print(value)

运行结果:

1
2
3
4
5
0
1
4
9
16

这个程序先定义了一个生成器函数。每次执行到 yield 时,函数就会产生一个结果,并暂停执行。等到需要下一个结果时,函数再从上次暂停的位置继续执行。

不过,你可能会发现:这个函数的代码并不复杂,只是循环取出数字、计算平方,然后产生结果。为了完成这么简单的工作,是否一定要专门定义一个函数呢?

Python 提供了另一种更简洁的写法:生成器表达式(Generator Expression)。

它可以把循环和计算过程写在一对圆括号中,不需要专门定义一个生成器函数。

为什么需要生成器表达式

假设我们需要处理 100 万个数字,并计算每个数字的平方。

如果使用列表推导式,可以这样写:

1
squares = [x * x for x in range(1000000)]

这行代码会计算从 0 到 999999 的所有数字的平方,并将结果保存到一个列表中。

也就是说,程序需要创建一个包含 100 万个计算结果的列表。

如果后续操作需要反复访问这些结果,列表就很方便。但如果我们只想依次处理每个结果,并不需要把所有结果同时保存在内存中,那么有没有更合适的方式?

这时就可以使用生成器表达式:

1
squares = (x * x for x in range(1000000))

注意观察这两行代码的区别:

1
2
3
4
5
# 列表推导式:使用方括号
squares = [x * x for x in range(10)]

# 生成器表达式:使用圆括号
squares = (x * x for x in range(10))

两者的计算规则很相似,但创建出来的对象不同。

列表推导式会立即计算所有结果,并将它们保存到列表中;生成器表达式则会创建一个生成器对象,等到需要结果时,再逐个计算。

这种按需执行计算的特点,称为惰性计算(Lazy Evaluation)。

需要注意,生成器表达式并不是创建时什么都不执行。Python 会在创建时求值最外层 for 右侧的表达式,以取得迭代所需的数据源;但循环体中的计算会延迟到获取结果时才执行。

生成器表达式的基本语法

生成器表达式的基本语法如下:

1
(表达式 for 变量 in 可迭代对象)

这几个部分分别表示:

  • 表达式:决定每次产生什么结果。
  • for 变量 in 可迭代对象:依次从数据中取出一个元素,并赋值给变量。
  • 圆括号 ():将整个表达式组合起来,创建生成器对象。

例如:

1
gen = (x * x for x in range(5))

我们可以把它拆开理解:

  • range(5):提供从 0 到 4 的数字。
  • x:每次接收一个数字。
  • x * x:计算当前数字的平方。
  • 圆括号:创建生成器表达式对应的生成器对象。

整个过程可以表示为:

flowchart TD
    A["创建生成器表达式"] --> B["取得最外层可迭代对象"]
    B --> C["创建生成器对象"]
    C --> D["开始获取结果"]
    D --> E["取出下一个元素"]
    E --> F["执行表达式计算"]
    F --> G["产生一个结果"]
    G --> H{"是否继续获取结果?"}
    H -->|是| E
    H -->|否| I["暂停获取,或在数据耗尽后结束"]

这里需要区分两个阶段:创建生成器对象,以及开始从生成器中获取结果。

创建生成器对象时,不会立即计算所有数字的平方;只有开始遍历或调用 next() 时,生成器才会逐步执行循环体中的计算。

创建生成器时发生了什么

先创建一个最简单的生成器表达式:

1
2
3
gen = (x * x for x in range(5))

print(gen)

运行结果类似:

1
<generator object <genexpr> at 0x...>

你可能会感到奇怪:为什么没有输出 0、1、4、9、16,反而出现了一段看起来比较复杂的内容?

这是因为 gen 保存的不是计算结果组成的列表,而是一个生成器对象。

它可以理解为一个能够按需提供数据的对象。创建它时,Python 会准备好生成器的执行状态,并取得最外层 for 所需的可迭代对象,但不会立即计算所有元素的平方。

其中的 0x... 是对象标识信息的显示形式,具体内容可能不同,不必特别关注。

创建时并不是所有操作都延迟执行

为了准确理解生成器表达式,我们来看一个例子:

1
2
3
4
5
6
7
8
def get_numbers():
print("正在准备数据")
return [1, 2, 3]


gen = (x * x for x in get_numbers())

print("生成器创建完成")

运行结果:

1
2
正在准备数据
生成器创建完成

为什么 get_numbers() 已经执行了,而 x * x 还没有计算?

因为 get_numbers() 位于最外层 for 的右侧:

1
for x in get_numbers()

Python 创建生成器表达式时,需要先求值这个表达式,取得可供迭代的数据源。因此,函数 get_numbers() 会立即执行,并返回列表 [1, 2, 3]。

但是,生成器表达式中的循环体还没有开始执行,所以 x * x 并没有计算。

如果接下来调用:

1
print(next(gen))

运行结果会继续出现:

1
1

此时生成器才开始从数据源中取出第一个元素 1,计算 1 * 1,并产生结果 1。

因此,准确地说:

生成器表达式会延迟执行循环体中的计算,但创建时仍会求值最外层 for 右侧的表达式,以取得迭代所需的可迭代对象。

使用 for 循环获取生成器中的数据

最常见的方式是使用 for 循环遍历生成器:

1
2
3
4
gen = (x * x for x in range(5))

for value in gen:
print(value)

运行结果:

1
2
3
4
5
0
1
4
9
16

程序的执行过程是这样的:

  • 第一次循环,获取数字 0,计算 0 * 0,产生结果 0。
  • 第二次循环,获取数字 1,计算 1 * 1,产生结果 1。
  • 第三次循环,获取数字 2,计算 2 * 2,产生结果 4。
  • 后续循环依次产生 9 和 16。
  • 所有结果产生完毕后,循环自动结束。

每次生成一个结果后,生成器都会保留当前的执行状态。下一次需要结果时,它会从上次暂停的位置继续执行,而不是重新从头计算。

使用 next() 一次获取一个结果

除了 for 循环,我们还可以使用 next() 函数手动获取生成器中的下一个结果。

例如:

1
2
3
4
5
gen = (x * x for x in range(5))

print(next(gen))
print(next(gen))
print(next(gen))

运行结果:

1
2
3
0
1
4

每调用一次 next(gen),就会尝试获取一个新的结果。

第一次调用,生成器产生 0;第二次调用,产生 1;第三次调用,产生 4。

为了更清楚地观察计算发生的时机,我们可以编写一个带有打印信息的函数:

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


gen = (square(x) for x in range(3))

print("生成器创建完成")

print(next(gen))
print(next(gen))

运行结果:

1
2
3
4
5
生成器创建完成
正在计算 0 的平方
0
正在计算 1 的平方
1

注意观察输出顺序。

创建生成器时,程序只输出了 生成器创建完成,没有出现任何平方计算信息。

第一次调用 next(gen) 时,才输出 正在计算 0 的平方,然后返回 0。

第二次调用 next(gen) 时,才计算数字 1 的平方,然后返回 1。

由于程序只调用了两次 next(),数字 2 的平方还没有计算。

这个实验说明,生成器表达式中的循环体会随着结果的获取逐步执行,而不是在创建生成器时一次性完成。

但是,如果最外层 for 右侧的表达式本身包含函数调用,那么这个函数调用仍然可能在创建生成器时发生。这一点不能与循环体的惰性执行混为一谈。

当所有结果都取完之后,如果再次调用 next(),就会触发 StopIteration 异常,表示生成器已经没有更多结果了。

例如:

1
2
3
4
5
gen = (x * x for x in range(2))

print(next(gen))
print(next(gen))
print(next(gen))

运行结果:

1
2
3
4
5
0
1
Traceback (most recent call last):
...
StopIteration

这是生成器正常结束时的行为,并不意味着生成器本身出现了故障。

实际开发中,如果只是想依次处理所有结果,通常使用 for 循环即可,不需要自己判断什么时候结束。

生成器表达式适合哪些场景

生成器表达式特别适合这样的任务:数据量可能很大,但我们只需要依次处理数据,而不需要同时保存全部结果。

对于 Linux 运维人员来说,处理日志就是一个很典型的例子。

假设我们有一个 access.log 文件,里面记录了 Nginx 的访问日志。我们希望找出所有包含 HTTP 500 状态码的日志行。

先使用下面的命令创建一个用于演示的小文件:

1
2
3
4
5
6
cat > access.log <<'EOF'
192.168.1.10 - - [21/Oct/2026:10:00:00 +0800] "GET / HTTP/1.1" 200
192.168.1.11 - - [21/Oct/2026:10:00:01 +0800] "GET /api HTTP/1.1" 500
192.168.1.12 - - [21/Oct/2026:10:00:02 +0800] "GET /login HTTP/1.1" 200
192.168.1.13 - - [21/Oct/2026:10:00:03 +0800] "POST /api HTTP/1.1" 500
EOF

这段命令会创建一个名为 access.log 的文件,其中包含四条模拟访问日志。

接下来,创建 Python 文件 filter_logs.py:

1
2
3
4
5
with open("access.log", "r", encoding="utf-8") as file:
error_logs = (line.strip() for line in file if " 500" in line)

for log in error_logs:
print(log)

运行程序:

1
python filter_logs.py

运行结果:

1
2
192.168.1.11 - - [21/Oct/2026:10:00:01 +0800] "GET /api HTTP/1.1" 500
192.168.1.13 - - [21/Oct/2026:10:00:03 +0800] "POST /api HTTP/1.1" 500

我们重点分析这一行:

1
error_logs = (line.strip() for line in file if " 500" in line)

它包含三个重要部分:

  • for line in file:依次读取文件中的每一行。
  • if " 500" in line:只保留包含指定文本的日志行。
  • line.strip():去掉每行首尾的空白字符,包括行尾的换行符。

创建 error_logs 时,Python 会取得文件对象 file 作为可迭代对象,但不会立即把整个文件读完,也不会立即执行所有日志行的筛选和处理。

当 for log in error_logs 开始遍历时,生成器才会逐步从文件中读取一行,检查这一行是否符合条件,并对符合条件的行执行 strip(),然后产生结果。

对于大型日志文件,这种写法可以避免额外创建一个保存全部匹配结果的列表。

不过,这里还有一个需要注意的地方:文件对象本身也是可迭代对象,读取会随着生成器的遍历而进行。因此,必须确保文件在生成器使用期间仍然处于打开状态。

上面的代码将生成器的创建和遍历都放在 with 代码块内,正是为了保证这一点。

另外,这个示例中的 500 只是为了演示生成器表达式的使用方式。在实际环境中,如果需要精确识别 HTTP 状态码,建议解析日志格式,而不是简单地依赖字符串包含关系。

生成器表达式与列表推导式有什么区别

现在我们已经知道,生成器表达式和列表推导式都可以通过循环创建数据,但它们的行为并不相同。

我们通过一个例子进行比较:

1
2
3
4
5
6
7
8
# 列表推导式
numbers_list = [x * x for x in range(5)]

# 生成器表达式
numbers_gen = (x * x for x in range(5))

print(numbers_list)
print(list(numbers_gen))

运行结果:

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

虽然最终显示的结果相同,但两者在创建数据时的行为不同。

对比项目列表推导式生成器表达式
语法使用 []使用 ()
创建时是否计算全部结果是否
是否保存全部结果是否
是否可以按需产生结果否,结果已创建是
能否直接多次遍历全部结果可以不可以直接重复消费同一个生成器
适合场景需要保存、反复访问结果依次处理数据、减少额外结果存储

这里还需要解释一下 list(numbers_gen)。

list() 会把生成器中剩余的结果全部取出来,并创建一个列表。因此,它适合用于演示或确实需要全部结果的情况,但如果我们的目标是避免一次性保存大量结果,就不应该随意将大型生成器转换成列表。

另外,列表推导式中的计算通常在创建列表时完成;生成器表达式中的循环体则是在遍历过程中逐步执行。它们不仅在内存使用方式上存在差异,在计算发生的时机上也不同。

生成器表达式只能使用一次吗

严格来说,生成器表达式创建的生成器对象是一次性迭代器。

例如:

1
2
3
4
5
6
7
8
9
gen = (x * x for x in range(3))

for value in gen:
print(value)

print("再次遍历:")

for value in gen:
print(value)

运行结果:

1
2
3
4
0
1
4
再次遍历:

第二次循环没有输出任何数字。

原因是第一次循环已经取完了生成器中的所有结果。生成器不会因为重新执行一次 for 循环,就自动从头开始计算。

如果确实需要再次遍历,可以重新创建生成器:

1
2
3
4
5
6
7
8
9
10
11
gen = (x * x for x in range(3))

for value in gen:
print(value)

gen = (x * x for x in range(3))

print("再次遍历:")

for value in gen:
print(value)

这样两次循环都能得到完整结果。

不过,如果数据来自一个已经关闭的文件,或者来自网络响应、数据库游标等有生命周期限制的资源,仅仅重新创建生成器表达式并不一定就能恢复数据。必须确保数据源本身仍然可用,必要时重新打开文件或重新获取数据。

因此,使用生成器时,要记住一个重要原则:生成器对象中的结果会随着遍历逐步被消费,遍历结束后不能直接从头再来。

生成器表达式与生成器函数有什么区别

前面我们已经学过生成器函数,它使用 def 定义,并通过 yield 产生结果。

例如:

1
2
3
def generate_squares():
for x in range(5):
yield x * x

使用生成器表达式,可以写成:

1
generate_squares = (x * x for x in range(5))

两种方式都可以逐个产生平方值,但适用场景有所不同。

生成器表达式适合简单的循环和计算逻辑,写法简洁,适合快速创建一个生成器。

生成器函数则适合更复杂的处理流程。例如,需要多步计算、多个条件分支、异常处理,或者需要在不同位置产生结果时,使用生成器函数通常更加清晰。

可以简单理解为:

  • 逻辑简单、只需要一个循环表达式时,优先考虑生成器表达式。
  • 逻辑复杂、需要多步处理时,优先考虑生成器函数。

它们并不是互相替代的关系,而是两种适用于不同场景的工具。

使用生成器表达式时需要注意什么

生成器表达式虽然简洁,但也有一些常见的注意事项。

不要误以为创建生成器就已经完成计算。

1
gen = (x * x for x in range(5))

执行这行代码后,生成器对象已经创建,但所有平方值并没有全部计算完成。只有在遍历或调用 next() 时,才会逐步产生结果。

同时,不要忽略最外层 for 右侧表达式的执行时机。如果这里调用了一个函数,该函数可能在创建生成器表达式时就会执行。

不要在需要反复访问结果时,直接把生成器当作列表使用。

生成器不能像列表一样通过 gen[0] 获取第一个元素,也不能直接通过 len(gen) 获取剩余元素数量。如果需要索引、长度或者多次访问,可以根据实际需求使用列表。

不要忽略生成器依赖的数据源。

如果生成器表达式依赖一个文件对象,那么在遍历过程中,文件必须保持打开状态。资源已经关闭时,继续读取可能会失败。

不要认为生成器一定比列表更快。

生成器的主要优势是按需产生结果,避免一次性保存全部结果。对于需要反复使用的数据,列表可能更方便;对于数据量很小的任务,使用生成器也不一定会带来明显的性能优势。

动手练习

学完之后,可以尝试下面三个小练习。

生成偶数

创建一个生成器表达式,依次产生 0 到 10 之间的偶数。

参考代码:

1
2
3
4
even_numbers = (x for x in range(11) if x % 2 == 0)

for number in even_numbers:
print(number)

运行结果:

1
2
3
4
5
6
0
2
4
6
8
10

这里的 x % 2 == 0 表示判断数字除以 2 后的余数是否为 0。满足条件的数字才会被产生出来。

转换服务器名称

假设我们有一组服务器名称,需要统一转换成大写形式:

1
2
3
4
5
6
servers = ["web01", "web02", "db01"]

upper_servers = (server.upper() for server in servers)

for server in upper_servers:
print(server)

运行结果:

1
2
3
WEB01
WEB02
DB01

这个示例说明,生成器表达式不只能计算数字,也可以转换字符串或处理其他类型的数据。

筛选高 CPU 使用率

假设我们已经获取了几台服务器的 CPU 使用率:

1
2
3
4
5
6
cpu_usage = [35, 92, 68, 88, 41]

high_cpu = (usage for usage in cpu_usage if usage >= 80)

for usage in high_cpu:
print(usage)

运行结果:

1
2
92
88

程序会逐个检查 CPU 使用率,只产生大于或等于 80 的数值。

需要说明的是,这里演示的是对已有列表进行筛选。如果数据最初已经全部保存在列表中,生成器只能避免额外创建一份筛选结果列表,并不能消除原始列表占用的内存。

写在最后

通过这篇文章,我们学习了生成器表达式的基本语法,以及如何使用 for 循环和 next() 获取生成器中的结果。

生成器表达式的核心特点是:使用简洁的圆括号语法,按需产生数据,而不是提前创建包含全部结果的列表。

理解惰性计算时,尤其要记住两个要点:

  • 创建生成器表达式时,Python 会求值最外层 for 右侧的表达式,以取得迭代所需的数据源。
  • 生成器表达式中的循环体会在获取结果时逐步执行,而不是在创建时计算所有结果。

在日常 Python 开发和 Linux 运维工作中,当我们需要筛选日志、转换数据或逐个处理大量记录时,生成器表达式可以让代码更加简洁,也有助于减少额外的内存占用。

但要记住,生成器对象中的结果会随着遍历逐步被消费。如果后续需要反复访问所有结果,就应该根据实际需求选择列表,或者重新创建生成器。

下一篇,我们将学习 Python 中另一个非常实用的内置函数:zip()。它可以把多个可迭代对象中的元素按照位置配对,帮助我们同时处理多组相关数据。