列表推导式与生成表达式

推导式(comprehension)是 Python 里最有辨识度的语法糖。当你在别人的代码里看到 [x*2 for x in nums if x > 0] 这种写法,第一反应是”哇,好 Pythonic”。这一篇要把推导式从最基础的列表推导式讲到字典推导、集合推导,以及”看似很像但性能差很多”的生成表达式。学完这一篇,你写的代码会立刻精简 30%。

一、为什么要有推导式

先看不用推导式的写法:

1
2
3
squares = []
for x in range(10):
squares.append(x * x)

用列表推导式:

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

好处:

  1. 简洁:一行搞定;
  2. 明确[...] 一眼看出是列表;
  3. 更快:CPython 里推导式比 append 循环快 20%-50%。

二、列表推导式的语法

1
[表达式 for 变量 in 可迭代对象 if 条件]
  • 表达式:每次迭代的输出;
  • for 子句:数据源,可以有多个;
  • if 子句:过滤,可选,可以有多个。

基本形式

1
2
3
4
5
6
7
8
# 平方
[x*x for x in range(5)] # [0, 1, 4, 9, 16]

# 过滤:只要正数
[x for x in [-2, -1, 0, 1, 2] if x > 0] # [1, 2]

# 表达式可以是函数调用
[str(x).zfill(3) for x in range(5)] # ['000', '001', '002', '003', '004']

多个 if

1
2
3
4
# 既大于 0 又是偶数
[x for x in nums if x > 0 if x % 2 == 0]
# 等价于
[x for x in nums if x > 0 and x % 2 == 0]

if-else 在表达式部分

1
2
# 正数保留,负数变 0
[x if x > 0 else 0 for x in nums]

注意语法位置:

  • 过滤iffor 后面;
  • 三元if-else 在表达式部分,for 前面。

多层 for

1
2
3
4
5
6
7
8
# 所有 (i, j) 对
[(i, j) for i in range(3) for j in range(3)]
# [(0, 0), (0, 1), (0, 2), (1, 0), ..., (2, 2)]

# 展平二维列表
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

读法:从左到右读 for,就像嵌套 for 循环从外到里。

但不要超过 2 层,否则可读性崩塌,改回普通循环。

三、字典推导式

用大括号,表达式部分写成 key: value

1
2
3
4
5
6
7
8
9
10
11
12
13
# 平方表
{n: n*n for n in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 反转字典
inv = {v: k for k, v in d.items()}

# 过滤字典
adults = {name: age for name, age in people.items() if age >= 18}

# 从两个列表 zip 出字典
d = {k: v for k, v in zip(keys, values)}
# 更简单:d = dict(zip(keys, values))

四、集合推导式

大括号加”没有冒号的表达式”:

1
2
3
4
{x % 3 for x in range(10)}       # {0, 1, 2}

# 从文本抽取所有独立单词
{w.lower() for w in text.split() if w.isalpha()}

五、生成表达式

把中括号换成小括号,就是生成表达式

1
2
3
4
5
6
7
gen = (x*x for x in range(10))
print(gen) # <generator object ...>
print(next(gen)) # 0
print(next(gen)) # 1

for x in gen:
print(x)

看起来像列表推导式,但它不会立刻算出所有元素——只在你需要时才产生下一个。

什么时候用生成表达式

巨大数据、只遍历一次、不需要索引 时用生成器:

1
2
3
4
5
# ❌ 内存爆炸:一次性生成 1 亿个数字
sum([x*x for x in range(10**8)])

# ✅ 内存友好:逐个产生
sum(x*x for x in range(10**8))

函数调用作为唯一参数时,括号可以省略:

1
2
sum(x*x for x in range(10))               # 括号省略
max((x.age for x in users), default=0) # 有多个参数就不能省

生成器还有很多深入内容,我们在 生成器 yield 深入 单独讲。

六、推导式的常见套路

1. 元素转换

1
2
uppercased = [w.upper() for w in words]
strs = [str(x) for x in nums]

2. 过滤

1
positives = [x for x in nums if x > 0]

3. 转换 + 过滤

1
even_squares = [x*x for x in range(10) if x % 2 == 0]

4. 从 (key, value) 序列构造字典

1
{k: v for k, v in pairs}

5. 交换字典 key/value

1
inv = {v: k for k, v in d.items()}

6. 分类计数(不用 Counter)

1
2
3
4
5
6
7
8
from collections import defaultdict
counts = defaultdict(int)
for w in words:
counts[w] += 1

# 用 Counter 更好
from collections import Counter
counts = Counter(words)

7. 二维矩阵

1
matrix = [[0] * cols for _ in range(rows)]

8. 转置矩阵

1
2
3
4
5
# 用 zip
transposed = list(zip(*matrix))

# 用推导式
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]

七、变量作用域

Python 3 里,推导式有自己的作用域,循环变量不泄漏:

1
2
3
4
5
6
7
[x for x in range(3)]
print(x) # NameError!

# 对比 for 循环
for x in range(3):
pass
print(x) # 2,泄漏了

生成表达式也有自己的作用域。这个隔离是好事,避免污染外层。

八、什么时候不要用推导式

推导式虽然简洁,但过度使用会毁掉可读性。以下情况请回归普通循环:

  1. 逻辑复杂——表达式部分有 3 个以上函数调用,或多个三元;
  2. 副作用——推导式的核心是”产生值”,如果只是想 print 或调用 side-effect 函数,用循环;
  3. 多层嵌套——超过两层 for 或含多个 if 时;
  4. 需要 break——推导式没法 break,配合 next(g for g in ... if ...) 更好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 反面教材:又长又难懂
result = [
(u.name.strip(), age)
for u in users
if u.active
for age in [max(0, u.age - u.exp)]
if age > 18
]

# ✅ 拆成普通循环,可读性强
result = []
for u in users:
if not u.active:
continue
age = max(0, u.age - u.exp)
if age > 18:
result.append((u.name.strip(), age))

九、性能对比

以计算 100 万个平方为例:

方式 耗时(相对)
列表推导式 1.0(基准)
for + append 1.3–1.5
map + list 0.9
生成表达式 + sum 内存 0,耗时 0.9

规律:推导式一般是”简洁+性能”的甜点。如果只是要 sum/max/any/all,生成表达式更省内存。

十、一个综合例子:日志聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# logs.py
"""从一堆日志行里聚合 IP 请求数(推导式风暴演示)。"""

logs = [
"2025-09-10 08:00:11 GET /index 200 1.2.3.4",
"2025-09-10 08:00:12 GET /login 200 5.6.7.8",
"2025-09-10 08:00:13 POST /login 401 1.2.3.4",
"2025-09-10 08:00:14 GET /profile 500 1.2.3.4",
"2025-09-10 08:00:15 GET /index 200 9.9.9.9",
]

# 1. 解析每一行为字段
parsed = [line.split() for line in logs]

# 2. 提取所有 IP
ips = [row[-1] for row in parsed]

# 3. 去重 IP 集合
unique_ips = {row[-1] for row in parsed}

# 4. IP → 请求次数
from collections import Counter
ip_counts = Counter(ips)

# 5. 只保留 5xx 错误的日志
errors = [row for row in parsed if row[3].startswith("5")]

# 6. 每个 IP 的成功率
attempts = Counter(row[-1] for row in parsed)
successes = Counter(row[-1] for row in parsed if row[3] == "200")
rates = {ip: successes[ip] / attempts[ip] for ip in attempts}

print("IP 请求:", dict(ip_counts))
print("错误:", errors)
print("成功率:", rates)

十一、常见陷阱

  1. if-else 位置搞错:过滤 if 在末尾,三元 if-else 在表达式部分。
  2. 在推导式里改变外部状态:推导式的语义是”产生一个新容器”,副作用会让人读起来头大。
  3. 同一变量名多层使用[x for x in xs for x in ys] 语法合法但含义奇怪,别写。
  4. 生成器只能遍历一次gen = (x for x in range(3)); list(gen); list(gen) 第二次是空的。
  5. 列表推导式内存占用:处理海量数据时先想想能不能改成生成表达式或 map
  6. 和 map/filter 的选择
    • 简单转换:[str(x) for x in nums]list(map(str, nums)) 都行,推导式更清晰;
    • 有过滤:推导式更好,别 list(filter(..., map(...)))

十二、小结与延伸阅读

  • 推导式是 Python 最有辨识度的语法糖,简洁又高效;
  • 列表 [...]、字典 {k: v ...}、集合 {...}、生成器 (...)
  • 结构:表达式 for 变量 in 数据 if 过滤
  • 三元 A if cond else B 放在表达式部分;
  • 多层 for、多个 if 都行,但超过 2 层就该拆;
  • 大数据、只遍历一次 → 生成表达式,省内存;
  • 复杂逻辑请回归普通循环。

延伸阅读:

下一篇 字符串格式化进阶 我们把字符串格式化的所有姿势彻底讲透。