前言
列表解析式(list comprehension,也译作列表推导式)是 Python 里辨识度最高的语法之一: 一行就把「遍历、过滤、变换」三件事写完。但它常被误用成「把 for 循环压成一行」的技巧, 结果写出层层嵌套、别人看不懂自己也改不动的表达式。本文讲三件事:怎么写(过滤、 多重循环、嵌套结构)、怎么选(和map/filter相比该用哪个)、写到哪该停(可读性边界)。另外会把作用域这条容易忽略的规则讲清楚。
有一个版本相关的旧账必须翻出来:在 Python 2 里,列表解析式的循环变量会泄漏到外层 作用域,[x for x in range(3)]执行后x仍然存在。Python 3 改成了独立作用域,循环 变量不会泄漏。Python 2.7 已于 2020 年 1 月 1 日停止维护,本文只写 Python 3 的行为, 示例适用于 Python 3.8 及以上。
一、基本形态
列表解析式由三部分组成:一个表达式、一个for子句、可选的if子句。
# 适用于 Python 3.8+
squares = [x * x for x in range(5)]
print(squares) # [0, 1, 4, 9, 16]
evens = [x for x in range(10) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
labels = [f"n={x}" for x in range(3)]
print(labels) # ['n=0', 'n=1', 'n=2']它等价于下面这个循环,只是把结果收集进了一个新列表:
# 适用于 Python 3.8+
squares = []
for x in range(5):
squares.append(x * x)
print(squares) # [0, 1, 4, 9, 16]两者结果相同,但解析式更快也更短,因为它不需要反复调用append那套方法查找。不过写法更短不等于永远更好读,后文会讲边界。
二、条件过滤与「在尾部还是在中部」
只做过滤时,if写在for后面:
# 适用于 Python 3.8+
words = ["a", "", "bb", "", "ccc"]
nonempty = [w for w in words if w]
print(nonempty) # ['a', 'bb', 'ccc']如果每一支都要给出不同的结果,用的是条件表达式,它写在表达式位置,不是if子句:
# 适用于 Python 3.8+
nums = [1, 2, 3, 4]
tags = ["偶" if n % 2 == 0 else "奇" for n in nums]
print(tags) # ['奇', '偶', '奇', '偶']if在for后面是过滤器,if ... else在for前面是「二选一」。这两个位置的if含义完全不同,写错位置不报语法错,但结果会出乎意料。
需要多重过滤就并列写多个if,它们是「与」的关系:
# 适用于 Python 3.8+
nums = range(20)
result = [x for x in nums if x % 2 == 0 if x % 3 == 0]
print(result) # [0, 6, 12, 18]三、多重循环与嵌套结构
多个for子句按从左到右的顺序嵌套,最左边的最外层。要生成「笛卡尔积」这种结果时 很方便:
# 适用于 Python 3.8+
pairs = [(x, y) for x in [1, 2] for y in ["a", "b"]]
print(pairs) # [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]它等价于:
# 适用于 Python 3.8+
pairs = []
for x in [1, 2]:
for y in ["a", "b"]:
pairs.append((x, y))
print(pairs)「嵌套列表」是另一回事,指的是表达式本身又是列表解析式,用来构造二维结构:
# 适用于 Python 3.8+
matrix = [[r * 3 + c for c in range(3)] for r in range(3)]
print(matrix) # [[0, 1, 2], [3, 4, 5], [6, 7, 8]]这种写法是安全的,因为每一轮外层循环都会新建一个内层列表,不存在「所有行指向同一 个列表」的问题。这一点和[[0] * 3] * 3的坑正好相反。
四、和map/filter的取舍
map和filter是内置函数,在 Python 3 里返回的是惰性迭代器,不是列表; 列表解析式是立即求值,马上把结果都算出来。这个差别决定了它们的适用场合。
| 维度 | 列表解析式 | map/filter |
|---|
| 返回 | 一次性算出整个列表 | 惰性迭代器,用到才算 |
| 过滤 + 变换 | 一个表达式里同时完成 | 通常要嵌套filter(...)与map(...) |
| 已有函数可直接用 | 要写成[f(x) for x in it] | 可直接写map(f, it) |
| 多条件复杂逻辑 | 直观 | 需要拼 lambda,可读性差 |
| 只算一次、立即用完 | 合适 | 也行,但要用list()物化 |
| 数据很大、只需逐个处理 | 会一次性占内存 | 更省内存 |
# 适用于 Python 3.8+
nums = [1, 2, 3, 4, 5]
# 解析式:过滤和变换写在一起,意图清楚
squares = [x * x for x in nums if x % 2 == 1]
print(squares) # [1, 9, 25]
# map + filter:需要拼两个函数/lambda
squares2 = list(map(lambda x: x * x, filter(lambda x: x % 2 == 1, nums)))
print(squares2) # [1, 9, 25]两种都对,但第二种读起来要从里往外拆,多数场景下解析式更清楚。反过来,如果已经有现成 的函数,map反而更短:
# 适用于 Python 3.8+
words = ["10", "20", "30"]
print([int(w) for w in words]) # [10, 20, 30]
print(list(map(int, words))) # [10, 20, 30]如果只需要一个「逐个产出」的序列、不想一次性占内存,用生成器表达式(圆括号而不是 方括号):
# 适用于 Python 3.8+
total = sum(x * x for x in range(1000)) # 不建中间列表
print(total) # 332833500在函数调用里充当唯一参数时,生成器表达式可以省掉一层括号。
五、作用域:Python 3 不泄漏循环变量
Python 3 的推导式在一个隐式的嵌套作用域里执行,目的就是让循环变量不「泄漏」到外层。 下面的代码在 Python 3 里不会改动外层的x:
# 适用于 Python 3.8+
x = "outer"
squares = [x for x in range(3)]
print(x) # outer,没有被覆盖
print(squares) # [0, 1, 2]需要知道的一个细节是:最左边那个for的「可迭代对象」表达式是在外层作用域里求值 的,只有循环体和后续子句在新的嵌套作用域里跑。所以下面这种写法是成立的:
# 适用于 Python 3.8+
data = [1, 2, 3]
result = [data[i] for i in range(len(data))]
print(result) # [1, 2, 3]如果反过来,在后面某个子句里引用被前面子句覆盖过的同名外层变量,读起来就会绕,最好 避免。
六、可读性边界与海象运算符
一个实用的经验规则:超过两层for,或者一个表达式里同时有嵌套的if ... else, 就改回普通循环。「一行能写完」不是目标,「一眼能看懂」才是。
Python 3.8 起可以用海象运算符:=在过滤条件里先把值算出来、再让表达式复用,避免 同一个调用写两遍:
# 适用于 Python 3.8+
def parse(s):
return int(s) if s.isdigit() else None
raw = ["1", "x", "2"]
values = [v for s in raw if (v := parse(s)) is not None]
print(values) # [1, 2]Python 3.8 之前没有这个写法,只能算两遍或者先做一步中间列表。
常见坑点
坑点 1:把if ... else写到for后面。
❌ 错误写法:
# 适用于 Python 3.8+
nums = [1, 2, 3]
# [x if x > 1 else 0 for x in nums] # 正确
# [x for x in nums if x > 1 else 0] # SyntaxError✅ 正确写法:
# 适用于 Python 3.8+
nums = [1, 2, 3]
mapped = [x if x > 1 else 0 for x in nums]过滤用尾部if,二选一用表达式位置的if ... else,位置不能混。
坑点 2:在解析式里改用原来的列表。
❌ 错误写法:
# 适用于 Python 3.8+
lst = [1, 2, 3]
# [lst.append(x) for x in [4, 5]] # 副作用写法,结果里全是 None用解析式做副作用,既浪费一个列表又难读。
✅ 正确写法:
# 适用于 Python 3.8+
lst = [1, 2, 3]
lst.extend([4, 5])
print(lst) # [1, 2, 3, 4, 5]坑点 3:闭包捕获循环变量。
❌ 错误写法:
# 适用于 Python 3.8+
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2]lambda 体里引用的是变量本身,等到调用时循环早已结束,i是最后一个值。
✅ 正确写法:
# 适用于 Python 3.8+
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]用默认参数把当前值固定下来。
坑点 4:多重循环的顺序搞反。
❌ 错误写法:
# 适用于 Python 3.8+
a, b = [1, 2], ["x", "y", "z"]
print([(x, y) for y in b for x in a]) # 最外层变成了 b✅ 正确写法:
# 适用于 Python 3.8+
a, b = [1, 2], ["x", "y", "z"]
print([(x, y) for x in a for y in b]) # 最左边的最外层左到右就是外到内,写反了结果顺序会变。
坑点 5:用in做过滤导致复杂度飙升。
❌ 错误写法:
# 适用于 Python 3.8+
a = list(range(1000))
b = list(range(500, 1500))
# 每个 a 的元素都要在 b 里线性查找
common = [x for x in a if x in b]✅ 正确写法:
# 适用于 Python 3.8+
a = list(range(1000))
b = set(range(500, 1500)) # 先转成集合,成员判断代价大幅下降
common = [x for x in a if x in b]把「被查的那一侧」换成集合,是这个模式最常用的优化。
坑点 6:以为解析式里的变量会泄漏到外面。
❌ 错误写法:
# 适用于 Python 3.8+
# 依赖 Python 2 的老习惯
squares = [i for i in range(3)]
# print(i) # NameError: name 'i' is not defined✅ 正确写法:
# 适用于 Python 3.8+
i = None
squares = [i for i in range(3)]
print(i) # None,外层变量不受影响需要保留循环变量就在外面另起一个名字显式赋值。
坑点 7:在解析式里调用会话外的可变量,逻辑失控。
❌ 错误写法:
# 适用于 Python 3.8+
seen = []
# 依赖顺序和副作用
# result = [x for x in [1, 2, 1] if x not in seen and not seen.append(x)]这行虽然能跑,但作者和读者都要在脑子里模拟每一步,属于典型的「能写但不该写」。
✅ 正确写法:
# 适用于 Python 3.8+
seen = set()
result = []
for x in [1, 2, 1]:
if x not in seen:
seen.add(x)
result.append(x)
print(result) # [1, 2]坑点 8:把生成器表达式当列表重复使用。
❌ 错误写法:
# 适用于 Python 3.8+
gen = (x * x for x in range(3))
print(list(gen)) # [0, 1, 4]
print(list(gen)) # [],生成器只能遍历一次✅ 正确写法:
# 适用于 Python 3.8+
gen = (x * x for x in range(3))
print(sum(gen)) # 5,一次遍历把想做的都做完生成器只能消费一次,需要反复使用就物化成列表。
总结
| 需求 | 推荐写法 |
|---|
| 简单变换 | [f(x) for x in it] |
| 只要满足条件的 | [x for x in it if cond] |
| 每项二选一 | [a if cond else b for x in it] |
| 多重循环 | 多个for,最左最外层 |
| 二维结构 | 嵌套解析式,每行独立创建 |
| 结果很大、只需逐个消费 | 生成器表达式( ... for ... ) |
| 已有现成函数 | map(f, it)也可以 |
| 逻辑超过两层 | 改回普通循环 |
解析式的价值在于把常见的「遍历 + 过滤 + 变换」压缩成一眼可读的形式;一旦要靠它 炫技,可读性反而下降。记住三条:if的位置决定语义、循环从左到右就是从外到内、 Python 3 里循环变量不泄漏。