page contents

你还在使用打桩来记录for循环吗?

你们是不是也干过这种事啊……我昨晚十点多吧,电脑还开着,群里突然有人丢一句:”哥,我这段 for 怎么跑着跑着就没动静了,我打桩打到手抽筋了“。我一看他那个代码,嗯……经典,for 里面 print(i),i 从 0 打到 200000,日志窗口跟瀑布似的往下冲,关键是你还看不出问题在哪,反正就是刷屏。然后他还说“我怀疑接口卡了”,我说你先别怀疑外面,先把你自己这个 print 刀给收了,真会把自己CPU砍死的。

attachments-2026-03-3m26w2eW69b215a7085a8.png你们是不是也干过这种事啊……我昨晚十点多吧,电脑还开着,群里突然有人丢一句:”哥,我这段 for 怎么跑着跑着就没动静了,我打桩打到手抽筋了“。我一看他那个代码,嗯……经典,for 里面 print(i),i 从 0 打到 200000,日志窗口跟瀑布似的往下冲,关键是你还看不出问题在哪,反正就是刷屏。然后他还说“我怀疑接口卡了”,我说你先别怀疑外面,先把你自己这个 print 刀给收了,真会把自己CPU砍死的。

我自己以前也这么干,别笑。后来线上排查多了,就那种“看日志一顿操作猛如虎,回头一看线索没搜全”的感觉特别熟……尤其你只靠业务关键字搜,根本漏掉真正的异常点那种。 所以我现在对“for 循环打桩”有点过敏,一般我会把“看进度”和“抓关键样本”拆开,不让它污染业务逻辑。

你先看最常见的:我只是想知道循环走到哪了,不是要把每一步都昭告天下。那就别 print 每一次,搞个“每 N 次打一次点”就行。

import logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)

def log_every(n: int):
    def _should_log(i: int) -> bool:
        return i % n == 0
    return _should_log

should_log = log_every(10_000)

total = 200_000
for i in range(total):
    # ... 你的业务逻辑
    if should_log(i):
        logging.info("loop progress: %d/%d (%.2f%%)", i, total, i * 100 / total)

就这一下,刷屏直接没了,而且日志能进文件,能分级别,能带时间戳,比 print 强太多。然后有人又会说:那我想看“卡住那一段”的输入数据咋办?你也别每次都打,抽样就行,随机抽样那种还挺好用。

import random
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

def sample(prob: float) -> bool:
    # prob=0.001 约等于千分之一抽样
    return random.random() < prob

for idx, item in enumerate(data):
    # ...处理 item
    if sample(0.001):
        logging.info("sample idx=%d item=%r", idx, item)

你看这就很像排查网络包的时候嘛,不可能把所有包都肉眼看一遍,你得抓“有代表性的那部分”,不然你看吐了。 for 也是一样的道理,你要的是线索,不是噪音。

再来一个我最近用得很顺手的:按“时间间隔”打点,而不是按“次数”。因为有时候一次循环快得飞起,有时候某个分支慢得离谱,你按次数打点反而错过了“慢点出现的瞬间”。

import time
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

class RateLogger:
    def __init__(self, interval_s: float = 1.0):
        self.interval_s = interval_s
        self._next = time.monotonic() + interval_s

    def hit(self, msg: str, *args):
        now = time.monotonic()
        if now >= self._next:
            logging.info(msg, *args)
            self._next = now + self.interval_s

rate = RateLogger(interval_s=0.5)

start = time.monotonic()
for i, item in enumerate(data):
    # ...处理 item
    rate.hit("running... i=%d elapsed=%.2fs", i, time.monotonic() - start)

这个好处是:你能稳定地看到“还活着”,并且看到耗时在怎么变。那种“跑着跑着没动静”基本一眼就能区分出来:到底是真的卡死,还是只是慢。

不过哈,最烦的还不是进度,是“到底慢在哪一步”。很多人 for 里 print 一堆变量,其实是在赌运气。你不如老老实实给关键段落打计时点,越土越有效。

import time
import logging
from contextlib import contextmanager

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

@contextmanager
def timer(name: str):
    t0 = time.perf_counter()
    try:
        yield
    finally:
        dt = time.perf_counter() - t0
        logging.info("%s took %.4fs", name, dt)

for i, item in enumerate(data):
    with timer("parse"):
        parsed = parse(item)
    with timer("compute"):
        out = heavy_compute(parsed)
    with timer("write"):
        save(out)

这段我跟你说,跟你在线上用链路 id 去把整条链路串起来看一个味儿:你不是要更多日志,你是要“时间线”。 而且你这样一分段,有时候你会发现根本不是 compute 慢,是 write 慢,甚至是你写入的时候触发了某个锁……你 print 变量永远猜不出来。

再狠一点的,别在循环里瞎猜了,直接上 profile。标准库就够用,别一上来就整一堆第三方工具把自己绕晕。

import cProfile
import pstats
from io import StringIO

def run():
    for item in data:
        parsed = parse(item)
        out = heavy_compute(parsed)
        save(out)

pr = cProfile.Profile()
pr.enable()
run()
pr.disable()

s = StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime")
ps.print_stats(20)  # 看前20个最耗时的函数
print(s.getvalue())

这个输出出来你基本就别狡辩了,谁最慢,名字写得清清楚楚。你要是还想更贴近“for 循环内部到底哪一行最慢”,那就把 heavy_compute 再拆细一点,或者在里面再加 timer,别硬靠 print。

对了,我还见过一种很离谱的“打桩”:循环里 print(item) 打对象,结果对象 repr 特别重,打印本身把程序拖死了……人还以为业务慢,实际是“你在打印时做了半个业务”。这就跟抓包那篇里说的,读到 1024 分片了你没处理好,最后你以为设备没发,其实是你自己解析错位。 所以我一般建议:打印就打印摘要,别打印整坨。

def brief(x, limit=120):
    r = repr(x)
    return r if len(r) <= limit else r[:limit] + "...<cut>"

for i, item in enumerate(data):
    if i % 5000 == 0:
        logging.info("peek i=%d item=%s", i, brief(item))

说到这你应该能感觉到哈:所谓“别在 for 里打桩”,不是说你啥都不记录,而是记录要讲究“成本”和“信息密度”。print 是最贵、最吵、最不可控的一种,尤其你把它塞进热循环里,等于给车轮胎上钉子,还问车为啥跑不快。

行了我先说到这……我这边咖啡凉了,等会儿还得回群里把那哥们的 print 删掉,不然他今晚又得盯着控制台发呆,嘴上还说“我怀疑网络”。嗯……先怀疑自己,真的。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

  • 发表于 2026-03-12 09:24
  • 阅读 ( 27 )
  • 分类:Python开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1875 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1875 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章