免费编程软件「python+pycharm」 链接:pan.quark.cn/s/48a86be2f…
凌晨一点,小林盯着屏幕上的爬虫日志发呆。他写了一段简单的代码,准备从某个公开API抓取十万条数据。循环跑了一个小时,进度条刚爬到三分之一。他忍不住在技术群里发了一句:"Python 循环这么慢的吗?"群里沉默了三秒,有人回了句:"用列表推导式试试,能快五倍。"
小林半信半疑地改了几行代码,再跑一遍,十五分钟跑完了。他盯着屏幕愣了半天,脑子里只有一个问题:凭什么?
这个问题,每个写过 Python 的人都应该问一次。列表推导式和 for 循环,表面上看只是语法糖的区别,底层却是 C 语言和 Python 虚拟机之间的速度对决。
先看一个最直观的例子。假设有一个包含一百万个整数的列表,想把每个数平方后存到新列表里。用 for 循环写:
numbers = list(range(1_000_000))
squared = []
for n in numbers:
squared.append(n * n)
换成列表推导式:
squared = [n * n for n in numbers]
两段代码干的是同一件事。但用 timeit 跑一下,结果能差出 30% 到 50%。有的场景下,差距甚至能拉到五倍 。这个差距从哪来?
拆开看执行过程就知道了。
for 循环那套写法,Python 解释器每一步都得干活。它要先在全局范围里找到 squared 这个列表,再找到列表的 append 方法,然后每循环一次,都得把 n * n 这个表达式算出来,再调用一次 append。这一套流程里,光属性查找就够累的——每次循环都得问一遍:"append 在哪?"
列表推导式不一样。它直接把整个逻辑打包成一个独立的代码对象,交给 Python 底层去执行。在 CPython 层面,这个循环是用 C 语言跑的,不需要每次迭代都查属性、调方法。字节码数量也少得多 。用 dis 模块反编译一下就能看到,for 循环那套生成的字节码指令比列表推导式多了好几行。指令越多,解释器要干的活就越多,速度自然就慢了 。
还有个细节很多人没注意:局部变量 vs 全局变量。列表推导式内部会优化变量查找路径,尽量走 LOAD_FAST 这种快的指令。而 for 循环里如果用到了全局变量,每次循环都得去全局表里翻一遍 。有人做过测试,光是消除点操作(把 append 存成局部变量),就能让 for 循环快 30% 。但再怎么优化,还是追不上列表推导式。
不过话说回来,列表推导式也不是万能神药。它有一个致命的弱点:一次性把所有结果都塞进内存里。如果数据量大到百万千万级别,生成一个巨大的列表可能会把内存撑爆。这时候 for 循环反而更可控,可以边处理边扔,或者用生成器表达式,把内存占用压到最低 。sys.getsizeof() 测一下就知道了,列表推导式占的内存可能是生成器表达式的几百倍 。
还有种情况,列表推导式反而更慢。比如你只是想循环执行某个函数,根本不关心返回值。有人测过,这种情况下 for 循环比列表推导式快 30% 以上 。道理很简单:列表推导式非得把一堆 None 塞进列表里再扔掉,白白浪费了创建列表的时间和内存 。
再看一个稍微复杂点的场景。如果循环体里有复杂的逻辑,比如嵌套条件、多次函数调用,列表推导式的优势会被稀释。因为无论用哪种写法,那些 Python 函数调用本身的开销是躲不掉的 。有学术研究也证实了这一点:在真实项目里,列表推导式不一定总是更快,具体表现取决于操作对象的类型和项目本身的特征 。
回到小林的故事。他后来把自己那段爬虫代码翻出来仔细看了一遍,发现之前用 for 循环的时候,每次迭代都要往列表里 append 一次数据。改成列表推导式之后,不仅代码短了,底层执行的 C 代码直接把数据塞进预分配好的内存区域里,省去了反复扩容列表的开销。再加上请求的数据量本来就大,一来一回差距就拉开了。
有人可能会问:五倍的性能差距是真的吗?
看几组实测数据就知道了。有开发者用十万条数据做测试,列表推导式耗时 0.009 秒,for 循环耗时 0.015 秒,差了 60% 。另一个测试里,跑一百万个元素的平方计算,列表推导式 0.78 秒,for 循环 1.12 秒,差了 43% 。还有更夸张的,有人用 CPython 3.12 测出来列表推导式比 for 循环快 30% 以上 。如果数据集再大一点,循环里再带点条件判断,五倍的差距确实可能出现。
所以写代码的时候怎么选?有个简单的原则:如果只是生成新列表,操作不复杂,数据量也撑得住内存,闭着眼用列表推导式。代码短,跑得快。如果循环里要做的事情太多,比如要 break、要处理异常、要嵌套多层逻辑,用 for 循环反而更清晰 。如果数据量大到内存报警,用生成器表达式,惰性求值省内存 。
还有个隐藏技巧:如果非得用 for 循环,可以把属性查找挪到循环外面。比如把 append 方法存成局部变量,能快不少 。这招在性能敏感的场景里很实用。
回到标题那个问题:五倍性能差距的秘密到底是什么?秘密其实就两条——C 语言级别的执行效率,加上省去了属性查找和函数调用的开销。列表推导式让 Python 解释器少干活,让底层 C 代码多干活。干活的姿势对了,速度自然就上去了。