Python技术迷

Python还能这么跑?不用改代码,性能飙升50倍

Python 脚本跑了 18 分钟,我第一眼没看代码,先看它是不是还在用 CPython 跑。

很多人优化 Python,上来就改循环、换列表推导、上多进程。不是不行,但有时候太早了。尤其那种纯 Python 写的字段清洗、日志扫描、规则匹配脚本,先换解释器,比你改半天代码更划算。

我说的是 PyPy。

同一份代码,不动业务逻辑:

python3 clean_order_log.py
pypy3 clean_order_log.py

有些场景差距能大得离谱。50 倍不是天天有,但纯 Python 热循环里,确实见过这种级别的变化。

我拿一个接近线上杂活的脚本说,别搞斐波那契那种玩具例子。

有一批订单日志,格式不算干净:

2026-06-20 10:12:31|uid=83921|order=PO20260620001|amount=129.50|status=PAID
2026-06-20 10:12:32|uid=83922|order=PO20260620002|amount=-1|status=BAD
2026-06-20 10:12:33|uid=abc|order=PO20260620003|amount=42.00|status=PAID

脚本要做三件事:解析、过滤脏数据、按用户汇总金额。

我平时会这么写,短一点,别把清洗脚本写成框架。

# clean_order_log.py
import sys
from decimal import Decimal

defpick(line):
    parts = line.rstrip("\n").split("|")
if len(parts) != 5:
returnNone

    box = {}
for item in parts[1:]:
        k, _, v = item.partition("=")
        box[k] = v

    uid = box.get("uid", "")
    amount = box.get("amount", "")
    status = box.get("status", "")

ifnot uid.isdigit():
returnNone

if status != "PAID":
returnNone

try:
        money = Decimal(amount)
except Exception:
returnNone

if money <= 0:
returnNone

return uid, money

defrun(path):
    total = {}
    bad = 0

with open(path, "r", encoding="utf-8") as f:
for line in f:
            row = pick(line)
if row isNone:
                bad += 1
continue

            uid, money = row
            total[uid] = total.get(uid, Decimal("0")) + money

for uid, money in sorted(total.items(), key=lambda x: x[0])[:20]:
        print(uid, money)

    print("bad_lines=", bad)

if __name__ == "__main__":
    run(sys.argv[1])

这段代码没什么高级东西,甚至有点土。

但它有个特点:大量字符串切割、大量条件判断、大量 Python 层循环。这个时候 CPython 的解释执行成本就比较明显。

直接跑:

time python3 clean_order_log.py order.log

再换 PyPy:

time pypy3 clean_order_log.py order.log

如果你的脚本主要时间都耗在 Python 自己的循环里,PyPy 的 JIT 会把热点路径编译优化掉。跑得越久,热点越稳定,它越占便宜。

这地方我一般会先看一眼耗时分布,不然容易误判。

# profile_once.py
import cProfile
import pstats

cProfile.run("import clean_order_log; clean_order_log.run('order.log')", "cost.out")

p = pstats.Stats("cost.out")
p.strip_dirs().sort_stats("cumtime").print_stats(15)

如果结果里全是这些东西:

ncalls  tottime  cumtime  function
8000000  9.821   18.443   pick
8000000  3.102    3.102   str.split
8000000  2.887    2.887   str.partition

那我会试 PyPy。

如果结果是这样:

function
requests.sessions.request
cursor.execute
pandas._libs...

那就别指望 PyPy 救命了。

请求慢是网络,SQL 慢是数据库,pandas/numpy 大头在 C 扩展,换解释器大概率没啥意思,甚至可能更麻烦。

PyPy 适合什么?

我自己的判断比较粗暴:

纯 Python 循环多,可以试。

规则判断多,可以试。

日志清洗、文本解析、批量校验,可以试。

大量依赖 C 扩展,先别急。

短命令行脚本,也别太乐观。PyPy 有预热成本,脚本一秒内结束,它还没热起来你就结束了。

还有个坑,别上来就把线上 Python 服务全换 PyPy。

先从离线脚本、定时任务、批处理开始。它们失败成本低,收益又明显。比如每天凌晨跑的对账清洗,原来跑一个小时,换 PyPy 后跑十几分钟,这种最舒服。

部署也简单:

pypy3 -m venv .venv-pypy
. .venv-pypy/bin/activate
pip install -r requirements.txt
pypy3 clean_order_log.py order.log

依赖装不上,别硬刚。说明这活不适合无脑换解释器。

我见过最亏的优化,是花两天把代码改得花里胡哨,最后提升 20%。其实第一步换个运行方式,十分钟就能知道有没有戏。

Python 慢不慢,不能只看语言。

先看它慢在哪里。

如果慢在解释器不断执行那堆重复的 Python 字节码,PyPy 就是个很直接的开关。

代码一行不改,先跑一把。

跑完再决定要不要动刀。