
咱们平时写Python的时候,生成列表是家常便饭。比如要搞个100以内的偶数列表,有人习惯用for循环+append,有人喜欢用列表推导式。不少人听说“列表推导式更快”,但具体快在哪?快多少?为什么快?今天咱们就掰开揉碎了说,从代码实测到底层原理,再到实际开发里的坑和面试常考题,一次讲透。
在聊速度之前,咱们得先明确两个概念:普通列表(这里特指“for循环+append构建的列表”) 和列表推导式。咱们先写两段代码,看看它们是怎么生成同一个列表的。
要生成“1000以内能被2整除的偶数列表”,普通写法是这样的:
# 普通列表:for循环 + append
def build_list_normal(size):
# 1. 先初始化一个空列表
result = []
# 2. 循环遍历每个元素
for x in range(size):
# 3. 满足条件就调用append添加元素
if x % 2 == 0:
result.append(x)
return result
# 调用函数,生成1000以内的偶数列表
normal_list = build_list_normal(1000)
print(normal_list[:10]) # 打印前10个元素:[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]这段代码逻辑很直观:先建个空列表,循环的时候判断条件,符合就用append把元素加进去。但你可能没注意到——每次循环满足条件时,都要调用一次 append 方法。
同样的需求,列表推导式写法更简洁:
# 列表推导式:一行生成列表
def build_list_comprehension(size):
# 格式:[表达式 循环条件 过滤条件]
return [x for x in range(size) if x % 2 == 0]
# 调用函数
comp_list = build_list_comprehension(1000)
print(comp_list[:10]) # 和上面结果一样:[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]看起来只是代码短了,但它的执行逻辑和普通循环完全不一样——它不需要每次调用 append ,而是在底层直接完成循环和元素添加。
光说不练假把式,咱们用Python的time模块实测一下,看看两者的速度差距。为了让结果更可信,咱们做两个优化:
import time
# 1. 定义两种构建列表的函数(前面已经写过,这里整合)
def build_list_normal(size):
result = []
for x in range(size):
if x % 2 == 0:
result.append(x)
return result
def build_list_comprehension(size):
return [x for x in range(size) if x % 2 == 0]
# 2. 定义时间测量函数:跑runs次,返回平均耗时
def measure_avg_time(func, size, runs=5):
total_time = 0.0
for _ in range(runs):
# 记录开始时间(perf_counter比time.time更精确)
start = time.perf_counter()
# 执行函数
func(size)
# 记录结束时间
end = time.perf_counter()
# 累加耗时
total_time += (end - start)
# 返回平均耗时
return total_time / runs
# 3. 测试不同数据量
test_sizes = [10**4, 10**5, 10**6, 10**7] # 1万、10万、100万、1000万
test_results = []
# 遍历每个数据量,计算耗时和提速百分比
for size in test_sizes:
# 普通列表耗时
normal_time = measure_avg_time(build_list_normal, size)
# 列表推导式耗时
comp_time = measure_avg_time(build_list_comprehension, size)
# 计算提速百分比:(普通耗时 - 推导式耗时) / 普通耗时 * 100%
speedup_rate = ((normal_time - comp_time) / normal_time) * 100
# 保存结果
test_results.append({
"数据量": f"{size:,}", # 格式化显示,比如10000→10,000
"普通循环+append耗时(秒)": round(normal_time, 6),
"列表推导式耗时(秒)": round(comp_time, 6),
"提速百分比": round(speedup_rate, 2)
})
# 4. 打印结果表格
print("=" * 90)
print("列表推导式 vs 普通循环+append 速度对比(Python 3.10环境,5次平均)")
print("=" * 90)
# 表头
print(f"{'数据量':<12} {'普通循环+append耗时(秒)':<25} {'列表推导式耗时(秒)':<25} {'提速百分比':<10}")
print("-" * 90)
# 表格内容
for res in test_results:
print(f"{res['数据量']:<12} {res['普通循环+append耗时(秒)']:<25} {res['列表推导式耗时(秒)']:<25} {res['提速百分比']:<10}%")我在自己的电脑上(i5-10210U,8G内存)跑了一遍,结果如下:
数据量 | 普通循环+append耗时(秒) | 列表推导式耗时(秒) | 提速百分比 |
|---|---|---|---|
10,000 | 0.000821 | 0.000685 | 16.57% |
100,000 | 0.007952 | 0.006641 | 16.49% |
1,000,000 | 0.078153 | 0.065214 | 16.56% |
10,000,000 | 0.772345 | 0.651289 | 15.67% |
从结果能看出来:
如果你用的是Python 3.10及以上版本,列表推导式的优势会更明显。这是因为Python 3.10对列表推导式的字节码生成逻辑做了优化,减少了中间临时变量的创建和销毁,进一步降低了开销。
咱们再做个对比测试:同样的代码,分别在Python 3.9和3.10上跑“100万数据”的场景,结果如下(我找了两台环境一致的电脑分别测试):
Python版本 | 普通循环+append耗时(秒) | 列表推导式耗时(秒) | 提速百分比 |
|---|---|---|---|
3.9 | 0.089215 | 0.075321 | 15.57% |
3.10 | 0.078153 | 0.065214 | 16.56% |
能看到:
简单说:Python版本越新,列表推导式的“快”越明显。
看完实测,你肯定想问:到底为啥列表推导式比普通循环快?其实核心就两个原因——减少方法调用开销和优化内存分配。咱们用大白话讲清楚。
普通循环里,每次满足条件都要调用result.append(x)。你可能觉得“调用个方法而已,能费多少时间?”但在Python里,函数/方法调用的开销比你想的大。
每次调用append,Python要做这些事:
result是不是列表(确认有append方法);x作为参数传给append;append的函数上下文(保存当前循环的状态,执行完再切回来);append的逻辑(往列表里加元素)。这些步骤每循环一次就要走一遍,累积起来就是不小的开销。
而列表推导式呢?它不需要调用append。Python解释器在处理列表推导式时,会直接在底层(C语言层面) 循环,满足条件就直接把元素加到列表里,没有“方法调用”这一步。相当于“直接动手干,不喊口号”,自然快。
列表在Python里不是“无限大”的,它有个“容量”概念:一开始容量小,元素满了之后,Python会自动申请一块更大的内存,把旧元素复制过去(这个过程叫“扩容”)。
比如普通循环用append时:
result = []的容量是0;每次扩容都要“申请新内存+复制旧元素”,这是很费时间的。
而列表推导式在执行前,会大概估算需要多少个元素(比如循环range(1000),过滤条件是“偶数”,大概需要500个元素),然后一次性分配足够的内存,不用反复扩容。相当于“先算好要装多少东西,直接买个够大的箱子”,省了反复换箱子的时间。
列表推导式虽然快,但不是所有场景都适合用。咱们写代码,除了效率,可读性和可维护性更重要——毕竟代码是给人看的,不是给机器看的。
比如下面这两个场景,你就能看出区别:
比如“生成100以内能被3整除的偶数列表”,列表推导式又快又简洁:
# 简洁明了,一看就懂
even_div3 = [x for x in range(100) if x % 2 == 0 and x % 3 == 0]如果用普通循环,反而多写几行:
even_div3 = []
for x in range(100):
if x % 2 == 0 and x % 3 == 0:
even_div3.append(x)这种场景,列表推导式完胜。
比如“生成一个列表,包含1-200中满足以下条件的数:能被5整除,且十位数字是3,且平方后大于1000”。如果用列表推导式,会写成这样:
# 虽然能跑,但读起来要琢磨半天
complex_list = [x for x in range(1, 201) if x % 5 == 0 and (x // 10) == 3 and (x**2) > 1000]如果用普通循环,加几行注释,可读性会好很多:
complex_list = []
for x in range(1, 201):
# 条件1:能被5整除
condition1 = x % 5 == 0
# 条件2:十位数字是3(比如30、35)
condition2 = (x // 10) == 3
# 条件3:平方后大于1000
condition3 = (x ** 2) > 1000
# 三个条件都满足才添加
if condition1 and condition2 and condition3:
complex_list.append(x)虽然代码长了点,但不管是你自己半年后看,还是同事接手,都能快速看懂逻辑。
总结平衡原则:
列表推导式虽然好用,但新手很容易踩坑。咱们总结几个最常见的问题,每个问题都给“错误代码”和“正确做法”。
有人想“用列表推导式打印元素”,结果生成了一个全是None的列表。比如:
# 错误:想打印0-4,结果生成None列表
wrong = [print(x) for x in range(5)]
print(wrong) # 输出:[None, None, None, None, None]原因:print(x)是“语句”,它的返回值是None。列表推导式会把每个元素的“表达式结果”收集起来,所以就成了None列表。
正确做法:
print,只保留表达式:right = [x for x in range(5)]
print(right) # 输出:[0, 1, 2, 3, 4]比如想生成一个二维列表[[0,1], [1,2], [2,3]],有人把嵌套顺序写反了:
# 错误:先内层循环,后外层循环,y未定义
wrong = [x for x in range(y, y+2) for y in range(3)]
# 报错:NameError: name 'y' is not defined原因:嵌套列表推导式的顺序是“从外到内”,和普通嵌套循环的顺序一致。普通循环是先外层y,再内层x;推导式也得是先y后x。
正确做法:
# 正确:先外层y,再内层x,顺序和普通循环一致
right = [x for y in range(3) for x in range(y, y+2)]
print(right) # 输出:[0, 1, 1, 2, 2, 3]
# 或者更直观的二维推导式
right_2d = [[x for x in range(y, y+2)] for y in range(3)]
print(right_2d) # 输出:[[0, 1], [1, 2], [2, 3]]有人想在推导式里修改外部变量,比如计数:
count = 0
# 错误:推导式里用赋值语句(count +=1)
wrong = [count += 1 for x in range(5)]
# 报错:SyntaxError: invalid syntax原因:Python的列表推导式里只能放“表达式”(比如x*2、x%2==0),不能放“赋值语句”(比如count +=1、a = b)。
正确做法:用普通循环修改变量:
count = 0
for x in range(5):
count += 1
print(count) # 输出:5比如想处理“1亿个整数”,有人直接用列表推导式:
# 错误:1亿个整数的列表,会占用大量内存(约400MB,视系统而定)
wrong = [x for x in range(10**8)]原因:列表推导式是“一次性生成所有元素”,会把所有元素存在内存里。数据量太大时,会导致内存不足,甚至程序崩溃。
正确做法:用“生成器表达式”(把[]换成()),按需生成元素,不占内存:
# 正确:生成器表达式,只在迭代时生成元素,不占内存
gen = (x for x in range(10**8))
# 迭代使用(比如只取前100个)
for x in gen:
if x > 100:
break
print(x)列表推导式是Python面试的高频考点,面试官经常会问“为什么快”“什么时候用”这类问题。咱们整理几个常见问题,给出“大白话回答模板”。
回答模板:
主要有两个原因。第一,普通循环每次加元素都要调用append方法,Python里方法调用要做很多准备工作(比如检查方法、传参数、切换上下文),开销大;而列表推导式不用调用append,直接在底层(C语言层面)循环加元素,没有这部分开销。第二,普通循环用append时,列表会反复扩容(满了就申请新内存、复制旧元素),费时间;列表推导式能大概估算需要的内存,一次性分配好,不用反复扩容。所以列表推导式更快。
回答模板:
Python 3.10主要优化了列表推导式的字节码生成逻辑,减少了中间临时变量的创建和销毁。比如之前版本会生成一些临时变量来存储循环状态,3.10去掉了这些多余的步骤,让字节码更简洁,执行效率更高。实测下来,同样的代码,3.10的列表推导式比3.9快约10%-15%,而且相对于普通循环的提速百分比也更高。
回答模板:
有两种情况不建议用。第一种是逻辑复杂的时候,比如多层嵌套循环、多个相互依赖的条件,或者需要中间变量存储计算结果——这时候列表推导式会变得很绕,别人看半天看不懂,维护成本高,不如用普通循环加注释。第二种是数据量极大的时候,比如生成1亿个元素,列表推导式会一次性把所有元素存到内存里,容易导致内存爆炸,这时候应该用生成器表达式(()),按需生成元素,省内存。
回答模板:
主要有三个区别。第一,返回类型不一样:列表推导式返回列表(list),生成器表达式返回生成器对象(generator)。第二,内存占用不一样:列表推导式一次性生成所有元素,存在内存里,数据量大时占内存多;生成器表达式不存元素,只在迭代时按需生成,几乎不占内存。第三,使用方式不一样:列表可以直接索引、切片(比如list[0]),生成器不能索引,只能迭代一次(迭代完就空了)。比如列表可以反复遍历,生成器遍历一次后再遍历就没元素了。
回答模板:
不能。因为Python的列表推导式里只能放“表达式”,而赋值语句(比如x = x*2、count +=1)不属于表达式,这样写会报语法错误(SyntaxError)。如果想修改元素,直接写表达式就行,比如[x*2 for x in range(5)];如果有复杂的赋值逻辑,建议用普通循环来写,更清晰也不会报错。
咱们聊了这么多,最后总结一下:
列表推导式是Python的“语法糖”,既简洁又高效,但用好它的关键不是“追求快”,而是“在合适的场景用合适的写法”。希望这篇文章能帮你彻底搞懂列表推导式,写出又快又好的Python代码!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。