我在 for 循环里一边遍历列表一边删元素,结果有些该删的没删掉、还漏处理了一些,我盯着这个忽好忽坏的结果排查了大半天的深度复盘

我用 for 循环遍历列表,遇到满足条件的就 list.remove() 删掉,直觉得很。可结果诡异:有些明明该删的没删掉、还漏处理了一些元素,遍历像在跳着走;两个该删的挨在一起就必漏一个,忽好忽坏难复现。深究 for 遍历列表的机制才懂:它靠索引往后走,我删掉索引 i 的元素后,后面元素整体前移一位,而循环下一步去看 i+1,那个前移到 i 的元素就被跳过了。在迭代一个集合的同时修改它的结构,会让游标位置和实际内容错位(列表静默跳过、dict/set 直接 RuntimeError)。这篇从遍历时修改为何错位讲起,到列表推导产出新集合/遍历副本/倒序删的正解、不可变式产出新值的思维,以及那句最戳心的——别在依赖一个状态做事时改变它,把读和写分开。

我在 for 循环里一边遍历列表一边删元素,结果有些该删的没删掉、还漏处理了一些,我盯着这个忽好忽坏的结果排查了大半天的深度复盘

这是一个让我对"遍历时修改集合"刻骨铭心的故事。我有一个列表,要遍历它,把其中满足某个条件的元素,删掉。我很自然地,写了一个最直觉的 for 循环:遍历这个列表,遇到该删的,就调 list.remove() 把它从列表里删掉。在我朴素的认知里,这天经地义——一边遍历、一边把不要的删掉,多直接。

可运行结果,处处透着诡异:有些明明满足删除条件的元素,竟然没被删掉,还留在列表里;而且,我用来"处理每个元素"的逻辑,似乎漏处理了一些元素——遍历像是"跳着走"的,有些元素被悄悄地跳过了。最气人的是,这个问题,还和具体的数据排布有关:如果两个该删的元素挨在一起,就必有一个删不掉;数据一变,表现就变,忽好忽坏。我当时百思不得其解:明明遍历了每一个元素啊,也明明对该删的调了 remove 啊,怎么会有的没删掉、有的没遍历到?我反复检查删除条件,逻辑没错。直到我去深究 for 循环遍历列表的底层机制,才恍然大悟,补上了关于"遍历"最重要的一课:问题的核心,是我在"遍历一个列表"的同时,又去"修改(删除元素)"这个列表——而这,会导致遍历的"位置"和列表的"实际内容",发生错位!具体来说:Python 的 for 循环遍历列表,底层是靠一个"索引(下标)"在往后走的——它先处理索引 0、再索引 1、再索引 2……可当我在遍历到索引 i 的元素、并把它 remove 删掉时,这个删除,会让它后面的所有元素,整体往前挪一位(原来索引 i+1 的,现在变成了索引 i)!可循环下一步,会去处理索引 i+1——而原来在 i+1 的那个元素,已经被挪到了 i,于是它就被跳过、没被遍历到了!这,就解释了我所有的困惑:为什么有的该删的没删掉?——因为它恰好"挪"到了被跳过的位置上,循环没遍历到它,自然没删。为什么有的元素漏处理了?——同样,因为删除导致后面元素前移,而循环索引继续后走,就跳过了那个前移过来的元素。我那个"一边遍历一边删"的直觉写法,根本性地违背了一条规则:你不能在"迭代一个集合"的同时,去"修改(增删)"这个集合——因为修改,会破坏迭代器赖以工作的、关于"位置/结构"的假设,让遍历乱套。

故障现场:删一个元素,后面的就整体前移、被跳过

我把这个"遍历时删除"的现场,用代码摊开给你看:

# ✗ 灾难: 一边遍历列表, 一边 remove 它的元素
nums = [1, 2, 2, 3, 4]
for x in nums:
    if x == 2:
        nums.remove(x)      # ✗ 遍历时删除, 会导致后面元素前移、被跳过!
print(nums)                 # [1, 2, 3, 4]  ← ??? 还有一个 2 没删掉!

# 为什么? for 循环遍历列表, 底层是"靠索引往后走"的:
#   遍历过程(索引 i 在走):
#   - i=0: nums[0]=1, 不删。 i→1
#   - i=1: nums[1]=2, 删! → nums 变成 [1,2,3,4], 后面的元素整体前移!
#          (原本 i=2 的那个 2, 现在被挪到了 i=1)
#   - i=2: 循环走到 i=2, nums[2]=3 —— 那个被挪到 i=1 的 2, 被跳过了!没遍历到!
#   - i=3: nums[3]=4, 不删。
#   → 第二个 2, 因为前移到了"已经走过"的位置, 被漏掉了, 没删成。

# 同理, "处理每个元素"的逻辑也会漏: 删一个 → 后面前移 → 索引后走 → 跳过一个。

# 与数据排布有关: 两个该删的挨在一起 → 必漏一个; 不挨着 → 可能碰巧没事。
#   → 忽好忽坏, 难复现, 极易上线后才暴露。

# 根因: 在"遍历(迭代)"一个列表的同时, "修改(删除)"了它。
#   迭代靠索引/迭代器走, 而删除改变了元素的位置和长度 → 遍历错位、跳过元素。
#   (注意: 对 dict/set 遍历时增删, Python 会直接抛 RuntimeError, 更严格)

看着这段代码和那个索引走位的过程,我才算真正理解了这个"忽好忽坏"的根源。问题的核心,是我在"遍历一个列表"的同时,又去"修改(删除元素)"了它——而这,导致了遍历的"位置",和列表的"实际内容",发生了错位要理解这一点,得知道 Python 的 for 循环遍历列表,底层是靠一个"索引"在往后走的(处理完索引 0,就去索引 1,再索引 2……)。而当我遍历到索引 1 的那个 2、并把它 remove 删掉时,关键的事情发生了:这个删除,让它后面的所有元素,整体往前挪了一位——原本在索引 2 的那第二个 2,现在被挪到了索引 1。可循环下一步,会去处理索引 2(而不是再看一次索引 1)——于是,那个被挪到索引 1 的第二个 2,就被彻底跳过、再也没被遍历到了!这就完美解释了我所有的困惑:为什么有的该删的没删掉?——因为它(第二个 2)恰好被"挪"到了循环已经走过的位置上,循环没再遍历到它,自然就没删。为什么有的元素漏处理了?——同理,删除导致后面元素前移、而循环索引继续后走,就跳过了那个前移过来的元素。这也解释了为什么它"和数据排布有关、忽好忽坏":两个该删的元素如果挨在一起,就必然漏掉一个;如果不挨着,可能就碰巧没出问题——所以它表现不稳定、难复现,极易在上线后、数据变了才暴露。归根结底:我那个"一边遍历、一边删"的直觉写法,根本性地违背了一条规则:你不能在"迭代一个集合"的同时,去"修改(增删)"这个集合。因为,迭代器(或索引)是依赖于集合的"结构/位置"来工作的;而你的增删,破坏了这个结构,让迭代器赖以前进的假设失效,于是遍历就乱套了——有的元素被跳过、有的被重复。(顺便一提:对列表,Python 是"悄悄地"出错、跳过元素;而对字典或集合,如果你在遍历时增删,Python 会更严格地直接抛出 RuntimeError,反而更容易发现。)

第一件事:搞懂"遍历时修改集合"会让迭代错位

定位到根源,我必须把"为什么遍历时不能改集合"这件事,彻底搞清楚:

为什么"遍历一个集合的同时修改它"会出错

# 迭代(遍历)是怎么工作的?
#   - 它靠一个"游标"(对列表是索引, 对其它是迭代器内部状态)在集合上移动。
#   - 这个游标, 依赖集合当前的"结构/长度/元素位置"来决定"下一个是谁"。

# 你在遍历时增删元素, 就破坏了这个依赖:
#   - 删元素: 后面的元素位置变了(前移), 游标却照常后走 → 跳过元素。
#   - 加元素: 可能重复遍历, 甚至无限循环。
#   - 改变长度: 游标可能越界, 或漏掉、或重复。
#   → 游标的"位置假设", 和集合的"实际状态", 错位了。

# 不同集合, 表现不同:
#   - list: Python 不报错, 但"静默地"跳过/重复元素(本文, 最坑——错了还不知道)。
#   - dict / set: 遍历时增删 → 直接抛 RuntimeError: dictionary changed size...
#                 (Python 主动检测并报错, 反而帮你发现问题)
#   - 其它语言: Java 的 ArrayList 遍历时改 → ConcurrentModificationException。
#   → 很多语言/集合, 都明确禁止"遍历时修改"(因为它本质就是危险的)。

# 一个通用规则: "别在迭代一个集合的同时, 修改这个集合的结构(增删)"。
#   (只改元素的"值"、不改"结构"通常没问题; 但增删元素一定要小心)

# 怎么办? 把"遍历"和"修改"分开:
#   - 遍历"副本", 修改"原集合"(遍历的和改的不是同一个)。
#   - 或: 不改原集合, 而是"产出一个新集合"(过滤/推导)。
#   - 或: 倒序遍历删除(删后面不影响前面的索引)。

# 核心: 迭代依赖集合结构稳定; 遍历时增删会破坏它, 导致跳过/重复/报错。
#   要么"遍历副本", 要么"产出新集合", 别原地边遍历边增删。

原理终于刻进脑子里了。要理解这个坑,得先知道迭代(遍历)是怎么工作的:它靠一个"游标"(对列表是索引,对其它集合是迭代器的内部状态),在集合上移动;而这个游标,依赖于集合当前的"结构、长度、元素位置",来决定"下一个该处理谁"。所以,当你在遍历时增删元素,就破坏了这个依赖:删元素,会让后面的元素位置变化(前移),而游标却照常后走,就跳过了元素;加元素,可能导致重复遍历、甚至无限循环;总之,改变了集合的结构,就让游标的"位置假设",和集合的"实际状态",发生了错位不同的集合,表现还不一样:list,Python 不报错,但会"静默地"跳过或重复元素(这是最坑的——你错了,却还不知道);对 dict/set,遍历时增删会直接抛 RuntimeError(Python 主动检测并报错,反而帮你及早发现);在其它语言里,比如 Java 的 ArrayList,遍历时改会抛 ConcurrentModificationException——可见,很多语言和集合,都明确禁止"遍历时修改",因为这件事,本质上就是危险的由此,我得到了一条通用的规则:"别在迭代一个集合的同时,修改这个集合的结构(增删元素)"(只改元素的"值"、不改"结构",通常没问题;但增删元素,一定要小心)。而正确的做法,核心是把"遍历"和"修改"分开:要么遍历"副本"、修改"原集合"(让遍历的和修改的,不是同一个东西);要么不改原集合,而是"产出一个新集合"(用过滤/推导);要么倒序遍历删除(从后往前删,删后面的不影响前面元素的索引)。归根结底:迭代,依赖集合结构的稳定;而遍历时增删,会破坏它,导致跳过、重复、或报错。所以,要么遍历副本,要么产出新集合,绝不要原地"边遍历边增删"——这,是我用一个"忽好忽坏、漏删漏处理"的 bug,补上的、关于遍历最关键的一课。

第二件事:正解——产出新列表,而不是原地边删边遍历

搞懂了根因——"遍历时增删破坏迭代"——正解就清晰了:最推荐、也最 Pythonic 的做法,是不去原地修改原列表,而是用"列表推导式"产出一个新的、只含要保留元素的列表;此外,也可以遍历原列表的一个副本(改原列表)、或倒序遍历来删除。核心,是把"遍历"和"修改"分开。

# 正解1(最推荐, 最 Pythonic): 用列表推导式, 产出"新列表"(只留要保留的)
nums = [1, 2, 2, 3, 4]
nums = [x for x in nums if x != 2]   # ✓ 产出新列表, 只含 != 2 的
print(nums)                          # [1, 3, 4]  ✓ 全对!
# 思路: 不"删掉不要的", 而是"留下要的"——产出一个全新的、正确的列表。
# (filter 也行: nums = list(filter(lambda x: x != 2, nums)))

# 正解2: 遍历"副本", 修改"原列表"(遍历的和改的不是同一个)
nums = [1, 2, 2, 3, 4]
for x in nums[:]:        # nums[:] 是副本! 遍历副本, 改原 nums
    if x == 2:
        nums.remove(x)   # ✓ 遍历的是副本(结构不变), 删的是原列表, 不会错位
print(nums)              # [1, 3, 4]  ✓
# (list(nums)、nums.copy() 都能得到副本)

# 正解3: 倒序遍历删除(从后往前, 删后面不影响前面的索引)
nums = [1, 2, 2, 3, 4]
for i in range(len(nums) - 1, -1, -1):   # 从最后一个索引往前
    if nums[i] == 2:
        del nums[i]      # ✓ 删 i 只影响 i 之后的, 而 i 之后的已经遍历过了
print(nums)              # [1, 3, 4]  ✓

# 字典/集合同理(它们遍历时改会直接 RuntimeError):
d = {"a": 1, "b": 0, "c": 2}
d = {k: v for k, v in d.items() if v != 0}    # ✓ 字典推导, 产出新字典
# 或遍历 key 的副本: for k in list(d): if d[k]==0: del d[k]

# 选择建议:
#   - 首选"产出新集合"(列表/字典推导、filter)——清晰、安全、Pythonic。
#   - 必须"原地改"时, 用"遍历副本"或"倒序删除"。
#   - 绝不"原地正序边遍历边增删"。

# 核心: 把"遍历"和"修改"分开。优先"产出新集合", 别原地边遍历边改。

这套正解,核心是把"遍历"和"修改"这两件互相干扰的事,分开来做正解1(产出新列表,最推荐、最 Pythonic):换一个思路——不去"删掉不要的",而是"留下要的";用一个列表推导式,产出一个全新的、只包含你想保留的元素的列表(nums = [x for x in nums if x != 2])。这个写法清晰、安全(它在遍历旧列表、构建新列表,两者不冲突)、而且非常 Pythonic(filter 也是同样的思路)。正解2(遍历副本,修改原列表):如果你必须原地修改原列表,那就遍历它的一个副本(for x in nums[:],nums[:] 是副本)——这样,你遍历的副本(结构不变)修改的原列表,不是同一个东西,就不会错位了。正解3(倒序遍历删除):从最后一个索引往前遍历删除——因为删除索引 i 的元素,只会影响 i 之后的元素位置,而 i 之后的,你已经遍历过了,所以不受影响。字典和集合也是同样的道理(它们遍历时改会直接抛 RuntimeError):用字典推导产出新字典,或遍历 key 的副本(for k in list(d))再删。选择建议是:首选"产出新集合"(列表/字典推导、filter)——它最清晰、安全、Pythonic;必须"原地改"时,用"遍历副本"或"倒序删除";而绝不要"原地正序地、边遍历边增删"归根结底:把"遍历"和"修改"分开;优先"产出新集合",而不是在原集合上边遍历边改。我那次的错误,正是把这两件事搅在了一起;而正解,就是把它们干净利落地分开。

下面这张图,对比了"原地边遍历边删"和"产出新集合"两条路径:

这张图的对比很清楚:左边红色那条,原地正序边遍历边 remove,删一个后面就前移、索引继续后走跳过前移来的元素,导致有的没删掉、有的漏处理、忽好忽坏;右边绿色那条,用推导式/filter 产出新集合——遍历旧的、构建只含要保留元素的新的,遍历和修改分离、互不干扰,结果全对、清晰安全。两条路的根本分野,在于你有没有把"遍历"和"修改"分开。

第三件事:其它"遍历时改集合"的坑

填平了列表这个坑,我系统排查了一遍"在迭代时改动东西"的其它常见坑:

# 其它"迭代时改动"的坑:

# 1. 遍历 list 时 remove/append(本文): 静默跳过/重复元素(列表不报错, 最坑)。

# 2. 遍历 dict 时增删 key: 直接抛 RuntimeError!
for k in d:           # ✗
    if cond: del d[k] # RuntimeError: dictionary changed size during iteration
# ✓ 遍历副本: for k in list(d): if cond: del d[k]
# ✓ 或字典推导产出新字典。

# 3. 遍历 set 时增删: 同样 RuntimeError。

# 4. 遍历时 append, 可能无限循环(边遍历边加, 永远加不完)。

# 5. 多个引用指向同一列表, 一处遍历、另一处(如别的线程/函数)修改 → 同样错位。
#    (并发场景下尤其危险)

# 6. 遍历"生成器"时, 改它依赖的数据源 → 行为难预料(生成器是惰性的)。

# 7. 修改"嵌套结构"时, 也要小心(改外层的同时遍历内层等)。

# 通用安全模式:
#   - 要过滤 → 推导式/filter, 产出新集合(首选)。
#   - 要原地删 → 遍历副本 list(x), 或倒序。
#   - 要原地加 → 先收集到另一个列表, 遍历完再 extend 回去。
#   - 并发 → 加锁, 或用并发安全的集合, 或遍历快照。

# 核心: "迭代一个东西" 和 "修改这个东西", 是天生冲突的两件事。
#   把它们分开(遍历副本 / 产出新的), 是处理这类问题的通用安全模式。

这一排查,让我对"迭代时改动"的各种坑,有了全面的警觉。除了遍历 list 时 remove(本文,列表不报错、静默出错,最坑),还有:遍历 dict 时增删 key(直接抛 RuntimeError: dictionary changed size during iteration——要遍历 list(d) 副本、或用字典推导);遍历 set 时增删(同样 RuntimeError);遍历时 append(可能无限循环,边遍历边加、永远加不完);多个引用指向同一列表,一处遍历、另一处(如别的线程、别的函数)修改(同样会错位,并发场景下尤其危险);遍历"生成器"时改它依赖的数据源(行为难预料,因为生成器是惰性的);以及修改嵌套结构时也要小心。通用的安全模式是:要过滤就用推导式/filter 产出新集合(首选);要原地删就遍历副本 list(x) 或倒序;要原地加就先收集到另一个列表、遍历完再 extend 回去;并发场景就加锁、或用并发安全的集合、或遍历快照。归根结底:"迭代一个东西"和"修改这个东西",是天生冲突的两件事;而把它们分开(遍历副本、或产出新的),正是处理这一整类问题的通用安全模式。把这个模式刻进习惯,这类静默又诡异的"遍历时改动"bug,就再也绊不倒你了。

第四件事:不可变式思维——产出新值,而非原地修改

这次踩坑,把我引向了一个更有价值的编程思想:与其"原地修改"一个东西,不如"产出一个新的"。我把它系统地梳理了出来:

# 不可变式思维: 优先"产出新值", 而非"原地修改"

# 两种风格:
# A. 原地修改(mutate in place): 在原对象上改(remove/append/改字段)。
#    nums.remove(x)        # 改的就是 nums 本身
# B. 产出新值(produce new): 不改原对象, 而是基于它算出一个新对象。
#    new = [x for x in nums if x != target]   # nums 不变, 产出 new

# 为什么"产出新值"往往更好?
# 1. 没有"遍历时修改"的冲突(本文): 遍历旧的、产出新的, 天然分离。
# 2. 不会"意外改到"别人共享的对象(原地改会影响所有引用它的地方)。
# 3. 更可预测、易推理: 输入→输出, 没有"副作用"埋伏在某处。
# 4. 对并发更友好: 不共享可变状态, 就少很多竞态。
# 5. 易于"回溯/对比": 旧值还在, 能对比前后、能撤销。

# 这正是"函数式编程"和"不可变数据"推崇的核心理念:
#   - 用 map/filter/reduce、推导式, 从旧数据"算出"新数据。
#   - 而不是 for 循环里, 对一个共享的可变对象, 修修补补。

# 当然, 原地修改也有它的位置:
#   - 性能敏感(大数据, 不想复制)、或确实需要"就地"语义时。
#   - 但默认, 优先"产出新值"——更安全、更清晰。

# 一句话: "别改它, 而是基于它造一个新的"——
#   这个简单的思维转变, 能避开"遍历时修改""共享对象被意外改"等一大类坑。

# 核心: 优先"产出新值"而非"原地修改"。
#   遍历时修改的坑, 本质是"原地修改"惹的祸; 换成"产出新值", 它就消失了。

这一思考,让我从一个具体的 bug,上升到了一个更普适的编程思想。处理数据,有两种风格:A. 原地修改(mutate in place)——在原对象上直接改(remove/append/改字段);B. 产出新值(produce new)——不改原对象,而是基于它,算出一个新对象(如用推导式 new = [x for x in nums if x != target],nums 本身不变)。为什么"产出新值"往往更好?有好几个理由:第一,没有"遍历时修改"的冲突(本文)——遍历旧的、产出新的,两者天然分离;第二,不会"意外改到"别人共享的对象(原地改,会影响所有引用着它的地方);第三,更可预测、易推理——输入到输出,清清楚楚,没有"副作用"埋伏在某个角落;第四,对并发更友好——不共享可变状态,就少了很多竞态;第五,易于回溯/对比——旧值还在,你能对比前后、能撤销。而这,正是"函数式编程"和"不可变数据"所推崇的核心理念:map/filter/reduce、用推导式,从旧数据"算出"新数据;而不是,在一个 for 循环里,对一个共享的可变对象,反复地修修补补当然,原地修改也有它的位置:在性能敏感(大数据、不想复制一份)、或确实需要"就地"语义时,原地改是合理的;但默认情况下,应优先"产出新值"——它更安全、更清晰归根结底,我领悟到一个简单却强大的思维转变:"别改它,而是基于它,造一个新的"——这个转变,能让你一举避开"遍历时修改""共享对象被意外改"等一大类坑。遍历时修改的坑,本质上,是"原地修改"惹的祸;一旦换成"产出新值"的思路,这个坑,就自然而然地消失了把"原地修改"和"产出新值"两种风格对比成一张表:

维度 原地修改(mutate) 产出新值(produce new)
遍历时改 会冲突,跳过/报错 遍历旧、产出新,无冲突
共享对象 会意外改到别处 原对象不变,安全
可推理性 副作用埋伏各处 输入→输出,清晰
并发 共享可变状态,易竞态 少竞态,更友好
适用 性能敏感/需就地 默认首选,更安全

第五件事:别在"观察一个东西"的同时"改变它"

这次踩坑,在认知层面给了我最大的纠偏——它让我领悟到一个超越编程的、普遍的原则。我把这层反思,沉淀了下来:

认知纠偏: 别在"依赖某状态遍历/观察"的同时, 去"改变那个状态"

# 我的误解(错误的):
#   我想当然地以为"一边遍历一边改"是直觉、自然的; 没意识到这两件事
#   会互相干扰——我"观察(遍历)"所依赖的东西, 被我自己"改变"了。

# 真相: "观察一个东西"和"改变这个东西", 同时进行, 会互相干扰
#   - 遍历依赖"集合结构稳定", 而你改了结构 → 遍历乱套(本文)。
#   - 这其实是个普遍现象: 当 A 依赖 B 的某个状态, 而你在 A 进行时改了 B,
#     A 就可能基于"已失效的假设"出错。

# 这个原则, 在很多地方都有体现:
#   - 遍历集合时改集合(本文)。
#   - 迭代器失效(C++ 容器增删使迭代器失效)。
#   - 回调/事件处理中, 改变正在被遍历的监听器列表。
#   - 渲染(读取状态)的同时改状态(很多 UI 框架要求别在渲染中改 state)。
#   - 读一个文件的同时写它。
#   → 共同模式: "基于某状态做事"时, 那个状态被"同时"改变了 → 出错。

# 正确的习惯:
#   1. 警惕"一边读/遍历 X, 一边写/改 X"的代码——它往往有坑。
#   2. 把"读/遍历"和"写/改"在时间上分开, 或操作不同的副本。
#   3. 大原则: 在依赖一个东西的稳定性做事时, 别同时破坏它的稳定性。

核心: 别在"遍历/依赖一个状态"的同时, 去"改变那个状态"。
  把"读"和"写"分开——这是避开一大类"自己干扰自己"的 bug 的通用智慧。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我想当然地以为,"一边遍历、一边改",是个直觉的、自然的写法;却没意识到,这两件事会互相干扰——我"观察(遍历)"所依赖的那个东西,被我自己"改变"了。可真相是:"观察一个东西"和"改变这个东西",如果同时进行,就会互相干扰——遍历,依赖"集合结构的稳定",而你改了结构,遍历就乱套了(本文);这其实是一个普遍的现象:当 A 依赖 B 的某个状态、而你在 A 进行的过程中改了 B,A 就可能基于"已经失效的假设"而出错。而这个原则,在许许多多的地方,都有体现:遍历集合时改集合(本文);C++ 容器增删导致的迭代器失效;在回调/事件处理中,改变正在被遍历的监听器列表;渲染(读取状态)的同时改状态(很多 UI 框架,都明确要求"别在渲染过程中改 state");读一个文件的同时写它——它们的共同模式,都是:"基于某个状态做事"的时候,那个状态,被"同时"改变了,于是出错。由此,我立下了几条习惯:第一,警惕"一边读/遍历 X、一边写/改 X"的代码——它往往藏着坑;第二,把"读/遍历"和"写/改",在时间上分开,或者让它们操作不同的副本;第三,记住那个大原则:在依赖一个东西的稳定性做事时,别同时去破坏它的稳定性。归根结底:别在"遍历/依赖一个状态"的同时,去"改变那个状态";把"读"和"写"分开——这,是避开一大类"自己干扰自己"的 bug 的、通用的智慧。我那个忽好忽坏的漏删 bug,正是一次典型的"自己干扰自己"。

维度 边读边改(踩坑) 读写分开(智慧)
遍历集合 边遍历边增删 遍历副本/产出新集合
UI 渲染中改 state 渲染只读,改在别处
文件 边读边写同一个 读写分离/写到新文件
本质 破坏所依赖的稳定性 依赖时不破坏它
结果 基于失效假设出错 稳定可靠

一套"遍历时要增删该怎么做"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"想在遍历一个集合时增删元素、该怎么做"的决策图,贴在了团队的规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:想遍历集合并增删,先问目的是什么——是过滤(留下满足条件的)就用推导式/filter 产出新集合(首选);必须原地改原集合的,删元素就遍历副本或倒序删、加元素就先收集到新列表、遍历完再 extend 回去。而如果操作的是 dict/set,绝不能直接遍历时增删(会 RuntimeError),要遍历副本或用推导。这条"先想目的、优先产出新集合"的决策链,现在是我们团队处理每一个"遍历时增删"需求时的准则。

我立下的几条遍历与修改规矩

这次"遍历时删元素漏删"的踩坑,让我把遍历与修改的注意事项,认真地立成了几条规矩:

  1. 绝不原地正序边遍历边增删。列表会静默跳过/重复元素(不报错最坑),dict/set 会 RuntimeError。
  2. 过滤优先用推导式/filter 产出新集合。最清晰、安全、Pythonic;不改原集合。
  3. 必须原地删就遍历副本或倒序。for x in lst[:] 改原 lst,或 range(len-1,-1,-1) 倒序 del。
  4. 必须原地加就先收集后 extend。别在遍历中 append,可能无限循环。
  5. dict/set 遍历改 key 要遍历 list(d) 副本。或用字典推导产出新字典。
  6. 优先"产出新值"而非"原地修改"。更安全、可预测、并发友好,避开一大类共享对象坑。
  7. 别在依赖一个状态做事时改变它。读和写分开——遍历集合时改集合、渲染时改 state 都是反面。

写在最后

这次"我一边遍历列表一边删元素、结果漏删漏处理"的经历,是我在 Python 路上,一次很经典、也很受用的成长。它教给我的,远不止"遍历时别改集合"这一条具体的技术经验,更是一个超越编程语言的、普遍的智慧——别在"依赖一个状态做事"的同时,去"改变那个状态";把"读"和"写"分开。我那个忽好忽坏的漏删 bug,根源就在于,我在"遍历(读)"一个列表的同时,又去"删除(写)"了它,亲手破坏了遍历所依赖的那个结构的稳定性,让自己干扰了自己。

所以,当你写下"一边遍历一个东西、一边修改这个东西"的代码时,请警觉起来——无论是遍历集合时增删、渲染时改状态、还是读文件时写它,这类"边读边写同一个对象"的写法,往往都藏着坑。更好的思路,是把"读"和"写"在时间上分开,或者干脆,换一种"产出新值"的思维:别去原地改它,而是基于它,造一个新的就像从列表里删元素,你只要用一句 [x for x in nums if ...] 产出一个新列表,就再也不会经历那种"漏删漏处理、忽好忽坏"的抓狂。从"原地修改"的直觉,到"产出新值"的清醒,从"边读边写"的隐患,到"读写分离"的稳健,是从一个"会写循环"的开发,走向一个"懂数据、思维严谨"的工程师,必经的修炼。愿你写的每一次遍历,都干净利落、不漏不重;也愿你我,永远记得,别在依赖一个东西的时候,亲手去动摇它。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 Agent 跑着跑着就开始胡言乱语、还动不动报上下文超限,最后发现是每一步的工具结果都被原样塞进了上下文、把窗口活活撑爆的深度复盘

2026-6-1 23:47:15

技术教程

我在循环里用 var 注册了几个回调,本想让它们各自打印 0、1、2,结果它们齐刷刷全打印了 3,我盯着这个诡异的结果排查了大半天的深度复盘

2026-6-2 0:00:34

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索