对20万元素跑列表推导式、生成器表达式、map+lambda共5种写法的计时对比,找出速度差异真正来自哪里,不是笼统的语法优劣。
都是对range(200000)里每个数乘2,写法不同。
代码N = 200000 timeit.timeit(f"[x*2 for x in range({N})]", number=20) # 列表推导式 timeit.timeit(f"list(x*2 for x in range({N}))", number=20) # 生成器表达式+list() timeit.timeit(f"list(map(lambda x: x*2, range({N})))", number=20) # map+lambda timeit.timeit(f"sum(x*2 for x in range({N}))", number=20) # 生成器表达式+sum(不建列表) timeit.timeit(f"sum([x*2 for x in range({N})])", number=20) # 列表推导式+sum
很多资料说"map比推导式快",本机结果恰恰相反。
结果(20次运行总耗时)listcomp build list: 0.0991秒 genexpr wrapped in list(): 0.1399秒 map+lambda wrapped in list(): 0.1650秒 ← 最慢 genexpr consumed by sum(no list): 0.1050秒 listcomp consumed by sum: 0.1105秒
map+lambda比列表推导式慢了约67%——原因不在map本身(map是C实现的内置函数,逐个取值很快),而在lambda x: x*2这个的可调用对象:map对每个元素都要发起一次的Python函数调用(哪怕函数体只有一条乘法),这次调用有的栈帧创建、参数绑定、字节码分派开销;而列表推导式里的x*2是内联在推导式自己的字节码里的,跟for循环共享同一个执行上下文,不需要为每个元素单独发起一次函数调用。
同样是生成器表达式,包一层list()和直接喂给sum(),耗时不一样。
对比genexpr wrapped in list(): 0.1399秒 genexpr consumed by sum(no list): 0.1050秒 ← 快了约25%
list()这一步:把生成器包进list()要为20万个元素分配并填充一个完整的列表对象;而sum()是边生成边累加,从来不需要同时持有全部20万个中间值,省下了这一整块列表分配和填充的开销。的结论是:判断该用哪种写法,关键不是"推导式还是生成器"这个语法差异,而是"最终是否需要一次性拿到完整的列表"——需要就用推导式(一步到位不用list()包一层),不需要(比如只是求和、找最大值、或者交给下一个消费者逐个处理)就用生成器表达式,能省下一整块内存分配。
每一步都是本文上面用timeit跑出来的原始结果。
timeit:对比列表推导式/生成器表达式/map+lambda共5种写法