python性能优化(1)
列表操作
同样造一个一万元素的列表:列表推导式最快,append 循环只慢一点点,用 + 一路拼接则慢出一个量级。
1 | import timeit |
前两者的差距其实很小,列表推导式只比 append 循环快 6% 左右,省掉的是每轮一次属性查找和方法调用,把它当成”数量级优化”就夸大了。真正拉开量级的是第三种写法:mylist + [i*i] 每次都新建一个列表、把已有元素整体复制一遍,整体是 O(n²),一万个元素就慢了 500 倍。
搜索成员时
set比list快。 列表O(n), 集合O(1).
1 | import timeit |
if 放在 for循环外面
如果判断条件跟循环变量无关,放在循环里就是每轮都白算一次。把它提到循环外面,只判断一次:
1 | # 慢:条件与 i 无关,却判断了 n 次 |
巧用 try/except
Python 里 try 在不抛异常时几乎没有开销,而每次都先判断反而要多做一次查找。所以”先做,出错再处理”(EAFP)通常比”先检查再做”(LBYL)快:
1 | # LBYL:每次都查一遍 key 在不在 |
前提是异常确实少见。如果十次里有八次都要抛异常,反过来会更慢——抛异常本身是有代价的。差距有多大值得实测一下(200 万次,CPython 3.14):
| 写法 | 键命中 | 键缺失 |
|---|---|---|
try/except |
0.019s | 0.159s |
if key in d |
0.036s | 0.022s |
d.get(key, 0) |
0.032s | 0.035s |
命中时 try/except 确实最快,但一旦键缺失就暴涨到 8 倍——而 d.get() 两头都稳。所以 dict 取默认值这个具体场景,最该用的既不是 LBYL 也不是 EAFP,而是被这两个词遮住的第三个选项:
1 | v = d.get(key, 0) # 不用判断,也不用抛异常 |
要往 dict 里累加就用 d[key] = d.get(key, 0) + 1,或者干脆上 collections.defaultdict / Counter。
顺带把”try 几乎没有开销”这句话的依据补上:Python 3.11 引入了零成本异常(zero-cost exception),把 SETUP_FINALLY 这类运行时指令换成了编译期生成的异常表——不抛异常时 try 块真的一条额外指令都不执行。这也意味着这条建议在 3.10 及更早的版本上并不那么成立,那时候进 try 是要付指令的。
神器 collections
标准库 collections 里的容器并不都是 C 实现的:deque、defaultdict、OrderedDict 来自 C 扩展 _collections,而 Counter、ChainMap、UserDict/UserList/UserString、namedtuple 都是纯 Python 写的。不过纯 Python 的那几个照样值得用——语义现成、代码更短,该借 C 的热点也借了:
1 | from collections import deque, defaultdict, Counter |
Counter 就是上面说的纯 Python 类,但它的计数热点走的是 C 辅助函数 _collections._count_elements,所以同样一份数据交给它数,还是比手写”先判断键在不在再累加”的 dict 循环快一点(实测 0.134 s vs 0.163 s)。
用 list 当队列时尤其要注意:pop(0) 和 insert(0, x) 都会移动整个列表,数据量一大就成瓶颈,换 deque 即可。