查看: 202|回复: 0

Python列表推导式语法、性能优化与常见陷阱实践

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
列表推导式是 Python 中从可迭代对象构建新列表的声明式语法,不是函数,也不是关键字,本质是语法糖。它把“要什么”放在前面,把遍历和过滤压缩到一行。用得好可以减少样板代码,用不好会引入共享引用、可读性下降等问题。下面按语法、执行顺序、性能、边界和实战场景梳理。

一、语法骨架与两个 if 的位置
最通用的形式是:
  1. [表达式 for 变量 in 可迭代对象 if 条件]
复制代码
扩展形式支持多个 for 和多个 if:
  1. [表达式
  2. for 变量1 in 可迭代对象1
  3. for 变量2 in 可迭代对象2
  4. if 条件1
  5. if 条件2]
复制代码
其中表达式必需,至少有一个 for,if 可选。需要特别区分:末尾的 if 是过滤器,只保留满足条件的元素;写在表达式位置的 if-else 是三元条件表达式,对每个元素做不同处理。例如:
  1. [f(x) if cond(x) else g(x) for x in items]
复制代码
这里的 if-else 不是过滤,不会减少元素数量。

二、基础写法与等价循环
无条件、无过滤的映射:
  1. squares = [x**2 for x in range(5)]
复制代码
等价于:
  1. squares = []
  2. for x in range(5):
  3.     squares.append(x**2)
复制代码
带过滤条件时,if 放在末尾:
  1. evens = [x for x in range(10) if x % 2 == 0]
复制代码
等价于在循环体内判断后再 append。带条件分支时,三元表达式放在开头:
  1. signs = ['+' if x >= 0 else '-' for x in [-2, -1, 0, 1, 2]]
复制代码
结果会对每个元素都产出值,而不是只保留正数。

三、嵌套循环、矩阵与展平
两个 for 可以生成笛卡尔积:
  1. pairs = [(x, y) for x in [1, 2] for y in ['a', 'b']]
复制代码
执行顺序是外层 for 在左、内层 for 在右:先固定 x=1,遍历所有 y;再固定 x=2,遍历所有 y。生成二维列表时,可以把内层推导式当作每一行:
  1. matrix = [[i * j for j in range(3)] for i in range(3)]
复制代码
展平嵌套列表时,先遍历子列表,再遍历子列表中的元素:
  1. nested = [[1, 2], [3, 4], [5]]
  2. flat = [item for sublist in nested for item in sublist]
复制代码
过滤与嵌套可以组合:
  1. result = [(x, y) for x in range(3) for y in range(3) if x < y]
复制代码
多个 if 条件等价于 and:
  1. [x for x in range(10) if x % 2 == 0 if x > 5]
复制代码
这段代码只保留同时满足两个条件的元素,即 6 和 8。

四、作用域:Python 2 与 Python 3 的差异
Python 2 中,列表推导式的循环变量会泄漏到外部作用域:
  1. x = 'outer'
  2. dummy = [x for x in [1, 2]]
  3. print(x)
复制代码
在 Python 2 中会输出 2,外部变量 x 被覆盖。Python 3 开始,列表推导式拥有自己的局部作用域,同样代码仍输出 outer。这一改进提升了变量安全性,也意味着不要把推导式内部的循环变量当作外部可用的结果。

五、性能:为什么通常快 10%~30%
列表推导式通常比等价的 for 加 append 更快,原因主要有三点:一是减少 list.append 方法调用开销,推导式在 C 层直接构建列表;二是 CPython 对列表推导式有专门字节码指令,例如 LIST_APPEND;三是某些情况下可以减少动态扩容带来的开销。可以用 timeit 做对比:
  1. import timeit
  2. t1 = timeit.timeit('[x**2 for x in range(1000)]', number=10000)
  3. t2 = timeit.timeit('''
  4. lst = []
  5. for x in range(1000):
  6.     lst.append(x**2)
  7. ''', number=10000)
  8. print(f'列表推导式: {t1:.4f}s')
  9. print(f'传统循环: {t2:.4f}s')
复制代码
通常 t1 小于 t2,但这类差异属于微优化。业务代码仍应优先保证可读性,只有在性能关键路径上才有必要用测试数据决定是否改写。

六、与 map、filter 的取舍
映射场景下,列表推导式通常比 map 加 lambda 更清晰,而且直接返回 list;map 返回的是迭代器,需要 list() 转换:
  1. squares = [x**2 for x in range(5)]
  2. squares = list(map(lambda x: x**2, range(5)))
复制代码
过滤场景同样如此:
  1. evens = [x for x in range(10) if x % 2 == 0]
  2. evens = list(filter(lambda x: x % 2 == 0, range(10)))
复制代码
把 map 和 filter 嵌套组合会降低可读性:
  1. result = list(map(lambda x: x**2, filter(lambda x: x > 0, range(-2, 3))))
复制代码
更推荐直接写成:
  1. result = [x**2 for x in range(-2, 3) if x > 0]
复制代码
列表推导式在这里承担了 map 和 filter 的组合职责,少了一层 lambda。

七、生成器表达式、enumerate 与字典推导式
如果不需要立即生成完整列表,可以用圆括号的生成器表达式,惰性求值,节省内存:
  1. gen = (x**2 for x in range(1000000))
  2. total = sum(gen)
复制代码
与 enumerate 结合可以直接拿到索引和值:
  1. indexed = [(i, char.upper()) for i, char in enumerate('hello')]
复制代码
Python 还支持集合推导式和字典推导式。例如用字典推导式把列表转成索引映射:
  1. words = ['apple', 'banana']
  2. index_map = {word: i for i, word in enumerate(words)}
复制代码

八、异常处理不能直接写进推导式
列表推导式不支持 try-except,需要把异常处理封装进函数:
  1. def safe_int(s):
  2.     try:
  3.         return int(s)
  4.     except ValueError:
  5.         return None
  6. data = ['1', '2', 'abc', '3']
  7. nums = [safe_int(x) for x in data if safe_int(x) is not None]
复制代码
这段代码能得到 [1, 2, 3]。需要注意,过滤条件里再次调用 safe_int 会重复执行转换;数据量大时,可以先完成转换再过滤,或者把清洗逻辑封装得更紧凑。这是从示例本身可以推导出的执行成本问题。

九、常见陷阱
第一,可变对象浅拷贝问题。不要用乘法创建多个子列表:
  1. matrix = [[]] * 3
  2. matrix[0].append(1)
  3. print(matrix)
复制代码
三个子列表共享同一个引用,结果会一起变化。正确做法是用列表推导式创建独立子列表:
  1. matrix = [[] for _ in range(3)]
  2. matrix[0].append(1)
  3. print(matrix)
复制代码
第二,过度嵌套会降低可读性。下面这种写法逻辑都挤在一行:
  1. result = [func(a, b) for a in list1 for b in list2 if cond1(a) and cond2(b)]
复制代码
可以拆成生成器函数,把条件判断和循环展开:
  1. def process():
  2.     for a in list1:
  3.         if not cond1(a):
  4.             continue
  5.         for b in list2:
  6.             if cond2(b):
  7.                 yield func(a, b)
  8. result = list(process())
复制代码
第三,不要把列表推导式用于副作用。下面写法会创建 [None, None, None],浪费内存:
  1. [print(x) for x in range(3)]
复制代码
打印、写文件等操作应使用普通循环:
  1. for x in range(3):
  2.     print(x)
复制代码
列表推导式适合构建数据,不适合执行操作。

十、落地场景
数据清洗可以组合 strip、title 和过滤:
  1. raw = [' Alice ', 'Bob\n', '', ' Charlie ']
  2. clean = [name.strip().title() for name in raw if name.strip()]
复制代码
路径处理中根据前缀决定是否替换:
  1. parent = '/home/user'
  2. files = ['./a.txt', 'b.log', './sub/c.py']
  3. resolved = [
  4.     x.replace('./', parent + '/', 1) if x.startswith('./') else x
  5.     for x in files
  6. ]
复制代码
提取 JSON 结构中的特定字段:
  1. users = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
  2. names = [u['name'] for u in users if u['age'] > 25]
复制代码
矩阵转置可以借助嵌套推导式:
  1. matrix = [[1, 2], [3, 4], [5, 6]]
  2. 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)]

适合使用列表推导式的场景是:逻辑简单,通常一到两层循环;目的是转换和过滤数据;代码仍然一眼能读懂。需要异常处理、存在复杂副作用、嵌套超过两层或逻辑难以快速理解时,应改用普通循环、函数或生成器。最后记住口诀:“前表达,中循环,后过滤;若要分支,三元放前面。”
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-22 14:38 , Processed in 0.026617 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部