列表推导式是 Python 中从可迭代对象构建新列表的声明式语法,不是函数,也不是关键字,本质是语法糖。它把“要什么”放在前面,把遍历和过滤压缩到一行。用得好可以减少样板代码,用不好会引入共享引用、可读性下降等问题。下面按语法、执行顺序、性能、边界和实战场景梳理。
一、语法骨架与两个 if 的位置
最通用的形式是:- [表达式 for 变量 in 可迭代对象 if 条件]
复制代码 扩展形式支持多个 for 和多个 if:- [表达式
- for 变量1 in 可迭代对象1
- for 变量2 in 可迭代对象2
- if 条件1
- if 条件2]
复制代码 其中表达式必需,至少有一个 for,if 可选。需要特别区分:末尾的 if 是过滤器,只保留满足条件的元素;写在表达式位置的 if-else 是三元条件表达式,对每个元素做不同处理。例如:- [f(x) if cond(x) else g(x) for x in items]
复制代码 这里的 if-else 不是过滤,不会减少元素数量。
二、基础写法与等价循环
无条件、无过滤的映射:- squares = [x**2 for x in range(5)]
复制代码 等价于:- squares = []
- for x in range(5):
- squares.append(x**2)
复制代码 带过滤条件时,if 放在末尾:- evens = [x for x in range(10) if x % 2 == 0]
复制代码 等价于在循环体内判断后再 append。带条件分支时,三元表达式放在开头:- signs = ['+' if x >= 0 else '-' for x in [-2, -1, 0, 1, 2]]
复制代码 结果会对每个元素都产出值,而不是只保留正数。
三、嵌套循环、矩阵与展平
两个 for 可以生成笛卡尔积:- pairs = [(x, y) for x in [1, 2] for y in ['a', 'b']]
复制代码 执行顺序是外层 for 在左、内层 for 在右:先固定 x=1,遍历所有 y;再固定 x=2,遍历所有 y。生成二维列表时,可以把内层推导式当作每一行:- matrix = [[i * j for j in range(3)] for i in range(3)]
复制代码 展平嵌套列表时,先遍历子列表,再遍历子列表中的元素:- nested = [[1, 2], [3, 4], [5]]
- flat = [item for sublist in nested for item in sublist]
复制代码 过滤与嵌套可以组合:- result = [(x, y) for x in range(3) for y in range(3) if x < y]
复制代码 多个 if 条件等价于 and:- [x for x in range(10) if x % 2 == 0 if x > 5]
复制代码 这段代码只保留同时满足两个条件的元素,即 6 和 8。
四、作用域:Python 2 与 Python 3 的差异
Python 2 中,列表推导式的循环变量会泄漏到外部作用域:- x = 'outer'
- dummy = [x for x in [1, 2]]
- print(x)
复制代码 在 Python 2 中会输出 2,外部变量 x 被覆盖。Python 3 开始,列表推导式拥有自己的局部作用域,同样代码仍输出 outer。这一改进提升了变量安全性,也意味着不要把推导式内部的循环变量当作外部可用的结果。
五、性能:为什么通常快 10%~30%
列表推导式通常比等价的 for 加 append 更快,原因主要有三点:一是减少 list.append 方法调用开销,推导式在 C 层直接构建列表;二是 CPython 对列表推导式有专门字节码指令,例如 LIST_APPEND;三是某些情况下可以减少动态扩容带来的开销。可以用 timeit 做对比:- import timeit
- t1 = timeit.timeit('[x**2 for x in range(1000)]', number=10000)
- t2 = timeit.timeit('''
- lst = []
- for x in range(1000):
- lst.append(x**2)
- ''', number=10000)
- print(f'列表推导式: {t1:.4f}s')
- print(f'传统循环: {t2:.4f}s')
复制代码 通常 t1 小于 t2,但这类差异属于微优化。业务代码仍应优先保证可读性,只有在性能关键路径上才有必要用测试数据决定是否改写。
六、与 map、filter 的取舍
映射场景下,列表推导式通常比 map 加 lambda 更清晰,而且直接返回 list;map 返回的是迭代器,需要 list() 转换:- squares = [x**2 for x in range(5)]
- squares = list(map(lambda x: x**2, range(5)))
复制代码 过滤场景同样如此:- evens = [x for x in range(10) if x % 2 == 0]
- evens = list(filter(lambda x: x % 2 == 0, range(10)))
复制代码 把 map 和 filter 嵌套组合会降低可读性:- result = list(map(lambda x: x**2, filter(lambda x: x > 0, range(-2, 3))))
复制代码 更推荐直接写成:- result = [x**2 for x in range(-2, 3) if x > 0]
复制代码 列表推导式在这里承担了 map 和 filter 的组合职责,少了一层 lambda。
七、生成器表达式、enumerate 与字典推导式
如果不需要立即生成完整列表,可以用圆括号的生成器表达式,惰性求值,节省内存:- gen = (x**2 for x in range(1000000))
- total = sum(gen)
复制代码 与 enumerate 结合可以直接拿到索引和值:- indexed = [(i, char.upper()) for i, char in enumerate('hello')]
复制代码 Python 还支持集合推导式和字典推导式。例如用字典推导式把列表转成索引映射:- words = ['apple', 'banana']
- index_map = {word: i for i, word in enumerate(words)}
复制代码
八、异常处理不能直接写进推导式
列表推导式不支持 try-except,需要把异常处理封装进函数:- def safe_int(s):
- try:
- return int(s)
- except ValueError:
- return None
- data = ['1', '2', 'abc', '3']
- nums = [safe_int(x) for x in data if safe_int(x) is not None]
复制代码 这段代码能得到 [1, 2, 3]。需要注意,过滤条件里再次调用 safe_int 会重复执行转换;数据量大时,可以先完成转换再过滤,或者把清洗逻辑封装得更紧凑。这是从示例本身可以推导出的执行成本问题。
九、常见陷阱
第一,可变对象浅拷贝问题。不要用乘法创建多个子列表:- matrix = [[]] * 3
- matrix[0].append(1)
- print(matrix)
复制代码 三个子列表共享同一个引用,结果会一起变化。正确做法是用列表推导式创建独立子列表:- matrix = [[] for _ in range(3)]
- matrix[0].append(1)
- print(matrix)
复制代码 第二,过度嵌套会降低可读性。下面这种写法逻辑都挤在一行:- result = [func(a, b) for a in list1 for b in list2 if cond1(a) and cond2(b)]
复制代码 可以拆成生成器函数,把条件判断和循环展开:- def process():
- for a in list1:
- if not cond1(a):
- continue
- for b in list2:
- if cond2(b):
- yield func(a, b)
- result = list(process())
复制代码 第三,不要把列表推导式用于副作用。下面写法会创建 [None, None, None],浪费内存:- [print(x) for x in range(3)]
复制代码 打印、写文件等操作应使用普通循环:- for x in range(3):
- print(x)
复制代码 列表推导式适合构建数据,不适合执行操作。
十、落地场景
数据清洗可以组合 strip、title 和过滤:- raw = [' Alice ', 'Bob\n', '', ' Charlie ']
- clean = [name.strip().title() for name in raw if name.strip()]
复制代码 路径处理中根据前缀决定是否替换:- parent = '/home/user'
- files = ['./a.txt', 'b.log', './sub/c.py']
- resolved = [
- x.replace('./', parent + '/', 1) if x.startswith('./') else x
- for x in files
- ]
复制代码 提取 JSON 结构中的特定字段:- users = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
- names = [u['name'] for u in users if u['age'] > 25]
复制代码 矩阵转置可以借助嵌套推导式:- matrix = [[1, 2], [3, 4], [5, 6]]
- transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
复制代码
十一、速查与使用边界
基本映射:[f(x) for x in items]
过滤:[x for x in items if cond(x)]
映射加过滤:[f(x) for x in items if cond(x)]
条件分支:[f(x) if cond(x) else g(x) for x in items]
笛卡尔积:[f(x, y) for x in A for y in B]
展平列表:[item for sub in nested for item in sub]
带索引:[(i, x) for i, x in enumerate(items)]
适合使用列表推导式的场景是:逻辑简单,通常一到两层循环;目的是转换和过滤数据;代码仍然一眼能读懂。需要异常处理、存在复杂副作用、嵌套超过两层或逻辑难以快速理解时,应改用普通循环、函数或生成器。最后记住口诀:“前表达,中循环,后过滤;若要分支,三元放前面。” |