小叶请AI写了一段程序,整理三条活动须知。第一次运行,结果表里有两条文字;补上一条缺失资料后,他又运行了一遍,结果变成三条。后来他修改其中一条原文,只运行显示结果的部分,屏幕上却仍然是旧内容。程序并没有随意改变规则,问题在于:这几次运行究竟从什么状态开始,又实际执行了哪些步骤?
看懂变量、条件和循环以后,还需要把它们连成一个发生在时间中的过程。代码说明可以怎样做;一次运行则是在具体数据、具体环境中,真正走过了其中一条路径。本章学习状态与执行过程,让小叶能够解释已经发生什么、眼前结果来自哪里,以及下一步应该继续还是重做。
以下使用另一组虚构教学资料,不与前章的报名和材料数量合账。三条须知编号互不重复,输入已经完整读入。非空缺的原文是文本,去掉首尾空白后仍有内容;None表示原文尚未取得。本轮只清理文字首尾的空白,并把结果保存在程序内存中的一张表里。这个简单操作便于观察过程,不代表须知的事实已经审核,也不代表文件已经保存或内容已经发布。
| 编号 | 原文 | 开始时的处理状态 |
|---|---|---|
| T11 | " 借用规则 " |
待处理 |
| T12 | None |
待处理 |
| T13 | " 开放时间 " |
待处理 |
小叶希望有原文的条目得到整理结果,缺原文的条目暂留在待补充状态;再次接续时,保留已经完成的结果,补做尚未完成的条目。这里的关键是让状态、结果和下一步动作彼此对应。
一、状态是下一步要依据的当前情况
程序的状态,可以先理解为执行到某个时刻时,继续工作所依赖的当前情况。它包括已经保存的值、名字所指向的对象,以及执行正处在哪个位置。涉及文件、网络或其他外部对象时,还要考虑它们与当前任务有关的情况。本章先在一个顺序执行的程序里观察内存中的数据。
“待处理”这个字段是状态的一部分,却不是程序的全部状态。同样写着待处理,一条须知有原文,另一条还没有,接下来能做的事就不同。结果表是否已有对应条目、当前循环正在看哪份资料,也会影响后续动作。不能把程序状态缩成某一个名为state的变量。
假设小叶只保存一句“已经做过一些”,下一次接续时就不知道做过谁、结果放在哪里、哪条还缺资料。如果保存每条资料的编号、原文、处理阶段和对应结果,下一步就有了具体依据。状态的作用,是把过去已经发生的事情中仍影响未来工作的部分留到现在。
它也不等于完整历史。只看到当前结果表有T11,未必知道它被整理过一次还是三次;只看到当前原文是新版本,也未必知道昨天的内容是什么。若任务需要回答“之前发生过什么”,就需要另外保存过程记录。当前值足以支持一些行动,却不一定足以还原所有过去。
同样的当前值也可能出现在不同执行位置。小叶刚开始准备处理T12时,结果表可以只有T11;处理T12途中遇到错误时,结果表也可能只有T11。前者还没开始这项工作,后者已经做过某些动作。若只比较结果表,而不问程序走到哪里,就容易把两个不同的现场看成同一个现场。
在本章这些固定输入、固定规则、没有其他程序同时修改数据的小例里,知道当前相关状态和下一步语句,就能推导接下来的变化。这种推导不是说一切现实程序都只由一张表决定。若下一步要读取时钟、取得随机数或等待外部回复,那些新输入也要进入解释。不能一方面遗漏它们,另一方面断言相同代码必有相同结果。
理解状态,对使用AI写程序的直接帮助是把问题问具体。与其只说“为什么这次不一样”,不如提供这次开始前的输入、上一次留下的结果,以及实际重新执行的范围。AI才能讨论一次可定位的过程,而不只是围绕代码猜测。
二、跟踪一次处理,看到动作如何留下结果
先只取T11,建立一份独立的初始记录和空结果表。这里用字典job保存这条任务,用字典cards按编号保存整理后的文字。下面每一步都在同一个小例中连续执行:
job = {"id": "T11", "source": " 借用规则 ", "state": "待处理"}
cards = {}
job["state"] = "整理中"
cleaned = job["source"].strip()
cards[job["id"]] = cleaned
job["state"] = "已整理"
print(job["state"])
print(cards)
输出为已整理,以及{'T11': '借用规则'}。文本的strip()方法返回去掉首尾空白后的新文本;它不会自动把新文本写回job["source"]。这里用cleaned接住返回结果,再明确写入cards。原文仍可保留,整理结果则存到约定的位置。
这段过程可以用一张执行跟踪表记录。跟踪表不是把所有变量每次都抄一遍,而是留下会改变下一步判断的内容:
| 已执行到的动作 | 任务状态 | 结果表中是否已有T11 | 现在能够确认什么 |
|---|---|---|---|
| 刚完成初始化 | 待处理 | 没有 | 有原文,尚未开始整理 |
| 标记开始处理 | 整理中 | 没有 | 已进入本条处理过程 |
| 得到去空白文本 | 整理中 | 没有 | 整理文本已在cleaned中,尚未写入结果表 |
| 写入cards | 整理中 | 有 | 约定位置已有整理结果 |
| 标记处理结束 | 已整理 | 有 | 本例要求的结果与结束标记已对应 |
第三行和第四行的区别尤其值得看清。得到一个值与把值保存到后续步骤会读取的位置,是两项不同的动作。如果程序只算出cleaned,后续却一直从cards取结果,那么这个中间值尚未完成交接。不能因为某一行“已经算对”,就认定后面自然能读到它。
同样标着“整理中”,结果可能尚未写入,也可能已经写入而结束标记还未更新。一个标签可以帮助概括阶段,但它未必包含阶段内部每一步的细节。需要接续中断任务时,应结合结果是否存在来判断,而不是让一个宽泛标签替代检查。

图中每个框表示完成相应动作之后的情况,箭头表示下一项动作。先取得文本、再写结果、最后标记结束,是这个小例实际执行的次序。把图画出来的价值,是能指出哪一步的结果还没有交给下一步,而不只是让“待处理到已整理”看上去很顺。
Python的执行模型说明区分程序文本、执行环境和名字绑定。本章不要求先掌握解释器内部实现;用当前记录、结果表和执行位置,已经可以检查这条具体处理链。
三、相同几句话,换个次序可能改变读到的东西
顺序执行意味着后面的语句通常看到前面语句已经留下的变化。表达式在被执行时取得所需的值,不会因为代码写在文件里,就提前与其他语句同时算好。先读旧值还是先更新再读,会影响记录表达的时间范围。
另设一个只观察合计的小例。小叶此前已经整理三条,现在又完成两条,准备显示更新后的总数:
completed = 3
shown = completed
completed += 2
print(shown, completed)
输出3 5。shown保存的是更新之前取得的那个整数,后面增加completed,并不会让shown自动变成新合计。若小叶希望显示本次更新后的数,应先更新completed,再把当前合计交给shown。问题不是“加二为什么失败”,而是准备显示的值取早了。
这里不需要让两个变量始终保持一样。若shown本来要记录“本轮开始前已经完成多少”,先取旧值正是正确安排。要判断先后对不对,必须先说明这个数代表哪个时点。变量有相同数值,不代表它们具有相同用途;暂时不相同,也不自动说明程序有错。
同样的关系出现在主例中。如果先把任务标成已整理,后面才清理文字和写结果,那么在中间某个位置,状态已经说完成,约定位置却还没有结果。正常一路运行到最后,可能暂时看不出区别;一旦中途失败,这个差异就会留下来。
因此,顺序的价值不是一条抽象的“先做A再做B”规则,而是让后一步依赖的事实先成立。在主例里,结束标记应有已经写入的结果作为依据;如果后续要读取cards,写入动作就应先于相应读取。顺序表达的是依赖,不是仅为了排版整齐。
有些动作也确实可以换序。若小叶分别整理两个互不共享可变数据、也没有先后要求的独立文本,先处理哪条可能不影响最终结果表的内容。判断能否交换,要检查前一步是否改变后一步要读取的东西,以及任务是否要求特定顺序。不能把单个算例中换序失败,推广成任何两句代码都绝不能调换。
还要分清计算顺序和文字里的时间描述。注释写着“最后更新”,并不会把一条放在前面的赋值推迟。AI生成的说明如果与实际语句顺序不一致,应以程序真正执行的安排检查结果,再修正代码或说明。自然语言意图不能替代码补做一次没有发生的更新。
四、判断结果记录的是当时情况,不是持续订阅
上一节里保存旧合计不会自动更新,比较得到的布尔值也一样。小叶可以把“目前是否可以处理”先算成一个变量,但它记录的是比较发生时的结果,不会持续监视原记录。
先看一份独立记录。在算出can_process之后,下面的代码完成了这条文本整理;最后再用先前的判断作一次检查:
job = {"id": "T11", "source": " 借用规则 ", "state": "待处理"}
cards = {}
can_process = job["state"] == "待处理"
cards[job["id"]] = job["source"].strip()
job["state"] = "已整理"
print(can_process)
print(job["state"] == "待处理")
两次分别打印True、False。第一次保存的判断没有错误:当时确实待处理。第二次现算的判断也没有错误:现在已经整理。这两个答案针对不同的时点,所以可以同时成立。
若后续动作的要求是“当前仍待处理才进入”,就应在采取动作前依据当前状态判断。若动作要保留的是“本轮开始时属于待处理的那一批”,先前的判断又可能正合用途。不能把所有提前保存的布尔值都称为过期错误;关键是后续使用是否仍符合它当初表示的事实。
这也是程序阅读中的一种常见误会。看到can_process这个名字,容易把它理解成永远准确的“现在可以处理”。实际上,一个好名字只能提示用途,不会替变量建立自动更新机制。只有代码重新求值、接收新输入或通过明确过程维护它,这个值才会变化。
对AI工作而言,类似问题会出现在“已经通过检查”“资料齐全”“可以提交”等标记里。若检查后又修改了正文,原来的通过标记仍可能留在记录中。它保存了过去某次检查的结论,却不一定覆盖当前新内容。要继续使用这个标记,就需要知道它对应的是哪份内容、哪项标准。
这并不要求每个小程序都建设复杂版本系统。小叶若只是处理三个文本,可以在原文修改后把该项重新设为待处理,再重新整理;若必须保留多次检查记录,则保存原文或结果的版本标识会更有帮助。具体记录多少,由准备作出的判断决定。
五、给状态变化一套与任务相符的规则
有了当前状态和执行次序,可以进一步描述状态转移:一项明确的动作或新事实,使任务从一种情况进入另一种情况。小叶可以为三条须知约定四个阶段:待处理、整理中、已整理、待补充。
这四个名称并非所有工作的通用阶段,而是当前任务的简化安排。待处理表示本轮还没有进入文字整理;整理中表示已经开始但尚未登记结束;已整理表示对应结果已放入cards;待补充表示因为没有原文,本次无法继续整理。它们要有可检查的含义,不能只是给同一个“未完成”换四种颜色。
| 当前情况 | 采取的动作或出现的新事实 | 接下来的处理 |
|---|---|---|
| 待处理,原文已取得 | 开始清理文本 | 进入整理中 |
| 待处理,原文未取得 | 确认本次缺原文 | 转为待补充 |
| 整理中,结果已经写入cards | 登记本条结束 | 转为已整理 |
| 待补充,后来取得原文 | 再次开始整理 | 进入整理中 |
| 已整理,原文发生有效修改 | 重新安排这一条 | 转回待处理 |
最后一行说明,状态转移不一定只能向前走。原文改变后,重新处理不是抹掉已经发生的历史,而是承认旧结果不再自动满足当前输入。若任务约定结果必须对应最新原文,就应重新建立这种对应。
先按原表完成第一轮处理。下面完整重建三条资料和空结果表,前面的独立小例不带入此处:
jobs = [
{"id": "T11", "source": " 借用规则 ", "state": "待处理"},
{"id": "T12", "source": None, "state": "待处理"},
{"id": "T13", "source": " 开放时间 ", "state": "待处理"},
]
cards = {}
for job in jobs:
if job["state"] not in ["待处理", "待补充"]:
continue
if job["source"] is None:
job["state"] = "待补充"
continue
job["state"] = "整理中"
cleaned = job["source"].strip()
cards[job["id"]] = cleaned
job["state"] = "已整理"
print(cards)
print([job["state"] for job in jobs])
第一行得到{'T11': '借用规则', 'T13': '开放时间'},第二行得到['已整理', '待补充', '已整理']。最后一行中的方括号写法是列表推导式:按jobs次序取出每条记录的state,组成一个新列表。它在这里仅用于显示状态,也可以换成逐条打印。
主例的结果与每条路径相符。T11有原文,经过整理、写结果和标结束;T12被发现缺原文,转为待补充,没有进入strip;T13继续处理,不因中间一条资料缺失就被遗漏。循环正常到达末尾,表示这一轮对现有条目的安排已执行,不表示三条原文都已齐全。
现在可以为“已整理”约定一项需要保持的关系:准备把条目交给后续步骤、或依据这个标记跳过处理时,cards里必须有该编号对应当前原文的整理结果。这类在程序指定位置应始终成立的关系,称为不变式。它要求保持某项关系,并不要求所有数据都不变。例如结果表可以从两条增加到三条,但已经标成已整理并交出的条目,不能缺少相应结果。
检查不变式,需要看它约束的东西。只有“已整理”的数量与cards长度相同,仍不足以证明编号一一对应;如果两边恰好各有两项,却指向不同编号,数量也可以相等。回到具体的T11、T12、T13,核对谁的结果在哪里,才能判断标记是否有依据。

这幅图描述当前任务允许的几条变化,其中“原文改变”使已整理返回待处理。它表达的是工作约定;真正产生转移的仍是程序中的赋值、结果写入和新资料输入。画出一条箭头,不等于代码已经实现了它。
六、输出、当前值与历史,是三种不同的观察
程序打印出来的内容,是某个时刻的一次输出。屏幕上已经出现的文字,通常不会随内存里的对象继续变化而自动重写。小叶看到一条“待处理”的旧输出时,要先问它是什么时候打印的,不能直接当作现在仍待处理的证据。
另设一个最短的观察例子:
state = "待处理"
print(state)
state = "已整理"
print(state)
屏幕依次出现待处理、已整理。第一行仍留在显示中,不表示程序同时持有两个互相矛盾的当前state。它们只是两次不同时间的观察。若小叶只截取第一行给AI,却没有说后面的更新已运行,AI就可能围绕一个不完整现场作解释。
反过来,当前值也不能自动补出历史。如果只保留最终state为已整理,并没有记录此前的步骤,就不能仅凭这个值确定什么时候开始、失败过几次、是否改过原文。需要追踪时,可以记录每次重要状态变化的对象、动作和结果,而不只是重复写一句“成功”。
快照是把某个时刻需要保留的情况保存下来。它与继续指向同一个可变对象不同。若小叶写history.append(job),只是把当前job对象的引用加入列表,后面再改这个字典,历史入口也会看到改变;这未必保留了当时的字段值。
看一个独立例子:
job = {"id": "T11", "state": "待处理"}
history = [job]
snapshot = job.copy()
job["state"] = "已整理"
print(history[0]["state"])
print(snapshot["state"])
输出分别是已整理、待处理。这里的copy创建了一个新的外层字典,而且本例字段值都是不可变文本,所以它能保留这些字段当时的值。若字典内部还有共享的可变列表,仅复制外层就不一定保留内部历史。前章关于浅复制的区别,在记录过程时也直接影响证据是否可信。
过程记录还需要适当的粒度。只记录“开始”和“结束”,可能足够统计一批正常任务;排查中途错误时,可能还需要知道结果是否已写入、结束标记是否已更新。记录不是越多越好,而是要能回答当前会影响接续的疑问。把所有临时数据倾倒到日志里,也可能使关键变化难以找到。
同样,日志声称“已完成”,不自动证明日志描述的事情发生了。若程序在实际处理之前就打印成功,这行输出只能证明打印动作发生过。要让它成为完成证据,应使记录时点、对象与已经得到的结果对应起来。
七、重新运行,是从原点再来,还是在旧结果上继续
小叶说“我又运行了一遍”,这句话至少有两种含义:从初始数据和空结果重新执行完整程序,或者保留当前环境,只再执行某一段代码。它们的起点不同,自然可能产生不同效果。
先用一份独立的编号清单观察重复追加:
ids = ["T11", "T13"]
selected = []
for identifier in ids:
selected.append(identifier)
print(selected)
for identifier in ids:
selected.append(identifier)
print(selected)
第一次得到T11、T13各一项,第二次在原列表后面又追加一遍,得到四项。循环没有偷偷多走一轮,而是同一段循环真的执行了两次,第二次也没有清掉第一轮结果。若每次都从selected = []重新开始,则每轮输出仍为两项;两种情形不能用同一句“同样代码”概括。
这使我们需要区分重新计算和接续处理。重新计算希望依据当前完整输入重建一份结果;接续处理希望保留已完成部分,只做尚未完成的部分。前者需要明确初始化范围,后者需要可靠的已完成依据。把接续程序里的结果表清空,可能丢掉本来要保留的结果;把重算程序的旧累计留下,又可能重复计算。
在一些任务中,还希望同一操作重复执行后,对指定对象产生的效果与执行一次相同。这叫幂等。这个名称描述的是重复后的效果,不是说程序只会运行一次,也不是说过程中绝不会再次输出日志。
例如,在同一份简单状态记录上再次赋值job["state"] = "待补充",就这个字段而言,执行一次与两次都留下待补充。向列表追加同一个编号则不同,重复操作会增加条目。若用编号作为字典键,把同一文本再次写到同一键,结果表的内容可以保持相同;但这只说明这张表在固定输入下的效果,不能保证与它相邻的发消息、扣额度等动作也只发生一次。
幂等的判断因此需要说清比较对象和输入条件。如果第二次输入的文字已经改变,覆盖同一个编号会得到新内容,这与“同一输入重复处理”的问题不同。也不能仅因为最后一行输出一样,就认定全部影响相同;输出之外的计数、记录和外部动作也可能已经增加。
主例用状态筛选来接续:已经整理的条目跳过,待处理或待补充的条目再检查原文。这种安排能否满足目标,取决于已整理标记仍有对应结果,而且原文没有发生需要重做的变化。状态字段本身不会替小叶维护这些条件。
如果AI建议“重复了就全部去重”,还应看重复发生在哪里。若只是生成了一份允许按编号合并的名单,去重可能合适;若两次已经分别发送了内容,事后删掉本地列表中的一个编号不会撤回第二次发送。解决办法必须针对实际被重复执行的动作,而不仅让最终表格看起来整齐。
八、后面报错,不代表前面什么都没发生
前面的主程序先检查原文是否为None,再执行strip。现在故意删去这个检查,观察一个不同的、会失败的程序。仍从三条原始资料和空cards开始,不沿用上一轮成功结果。
下面的try表示先尝试执行缩进范围里的代码;遇到指定的AttributeError后,转到except里的处理。这里仅用它显示错误现场,让程序还能把当时的记录打印出来。完整的异常处理与恢复设计在后续章节展开。
jobs = [
{"id": "T11", "source": " 借用规则 ", "state": "待处理"},
{"id": "T12", "source": None, "state": "待处理"},
{"id": "T13", "source": " 开放时间 ", "state": "待处理"},
]
cards = {}
try:
for job in jobs:
job["state"] = "整理中"
cleaned = job["source"].strip()
cards[job["id"]] = cleaned
job["state"] = "已整理"
except AttributeError:
print("本次处理在取得整理文本时中断")
print(cards)
print([job["state"] for job in jobs])
错误发生在T12:None没有这里使用的strip方法,因而产生属性错误。打印现场可以看到,cards只含T11的借用规则,三个任务状态依次是已整理、整理中、待处理。T11此前已经写入的结果仍在;T12已改成整理中,却没有得到文本;T13还没有进入循环体。
这不是三条都失败,也不是三条都没有处理。错误终止了当前这一段正常流程,没有自动把之前的字典写入和状态更新撤销。Python的异常处理教程说明,发生匹配异常时会跳过try中剩余部分,转入对应处理。这里的代码没有安排回滚,已经发生的修改就不能被当作从未发生。
也不要把这个结果推广到所有错误。若整个脚本在开始执行前就因为语法不合法而不能编译,前面的普通语句可能根本没有开始;若某个系统明确提供了事务回滚,也可能有另外的恢复规则。判断现场要看错误发生的阶段和已有机制,不能只凭“报错了”三个字推断所有变化都还在或全部消失。
回到这次具体中断,最有用的追问是最后一个成功完成的动作是什么。T12已经被标成整理中,下一步取得整理文本时失败,所以对应结果没有写入;后面的结束标记与T13处理都未发生。沿实际顺序定位,能够说明为什么各部分留下不同状态。
若小叶此时把原文补上,只重新运行最后的打印,仍然不会得到T12结果。打印没有执行清理与写入。若只把状态改成已整理,缺失结果也不会自行产生。正确的后续动作必须补做尚未完成的那部分处理,而不是只更新描述它的文字。
这里还藏着一个接续问题:第五节的正常程序只接纳待处理和待补充,若把中断后的T12原样交给它,整理中状态会被跳过。这次漏做T12,是因为入口条件排除了它;循环即使正常运行,也不会进入它的文本整理步骤。接续前需要检查这个中途状态,再决定将它归到待补充,还是安排继续处理。
九、接续要先辨认现场,再选择保留和重做
面对刚才的中断,小叶可以先保留T11已有结果,检查T12的原文和结果,再处理尚未开始的T13。不是所有条目都要从零重做,也不是看到已有状态就全部照信。
在这个已知现场里,T12仍没有原文,cards中也没有它的结果,因此可以把它从整理中归到待补充。T13仍待处理且有原文,可以正常整理。T11已有与原文相符的结果和结束标记,可以保留。这样的接续分别对应三种不同情况,实际解决的是中断留下的入口状态问题。
如果T12后来补上" 归还时间 ",再让它进入正常的清理、写入、标结束过程,就会形成第三条结果。仅把原文从None换成文本,还没有执行这些动作;但它确实消除了此前无法整理的原因。恢复安排需要把新的可用条件与待执行步骤接起来。
原文补齐以后,假设T12在另一次处理中断,而且中断发生得更晚:它的结果已经写入cards,只是还没来得及更新已整理标记。此时同样的整理中标签,代表的现场与上一节不同。检查结果表可以发现区别。本例的文本清理和按编号覆盖在相同原文下可以重复,重新整理能够得到相同的结果表内容;但是否需要重复,应根据结果是否可信和任务成本决定。
若把任务换成向同学实际发送通知,恢复就多了一层外部状态。发送已经发生、但本地来不及记录,也可能留下一个未完成标记;只凭本地没有成功记录,不能确定通知没有发送。此时应查可用的发送回执或目标端状态,仍无法确认就保留不确定性,而不是把缺少记录直接解释成动作没发生。本章的内存文字例子没有这种外部发送,不能借它保证任意重试都没有重复影响。
检查点是为后续接续保存的一组必要信息。它应能说明哪份输入、哪些结果已经保留、接下来从哪里处理。只保存“做到了第二条”有时不够:第二条可能刚开始,也可能结果已写完;原文版本还可能改变。保存什么,要围绕准备从哪个具体步骤继续。
检查点也需要有可靠的存放位置。只留在当前进程内存里的字典,在该进程结束后不能被假定仍然存在。若希望重启后接续,就要把需要的信息明确保存到能够重新读取的位置,并在新运行中实际加载。文件怎么写、格式如何选择将在C07展开;眼前先分清“打印过”“内存里有”和“已保存且能重新取得”。
从头重算有时比维护检查点更简单。只有三段很短的原文、清理没有外部影响、源资料又完整保留时,重新建立结果表可能更容易核对。若处理一次要花很多时间,或动作无法随意重复,保存足够的过程信息就更有价值。这是围绕实际成本和效果选择方式,不是所有任务都必须先建设复杂恢复框架。
十、分段运行时,屏幕顺序不等于执行顺序
大学生常在交互式环境里分段运行代码。一个片段可能只改输入,另一个片段负责处理,第三个片段负责显示。页面从上到下的排列有助于阅读,但当前内存中保存什么,取决于哪些片段真正执行过、以什么顺序执行。
用三个很短的片段来观察。片段A给出source,片段B整理后保存cleaned,片段C显示cleaned。第一次依次运行它们;第二次实际执行新输入赋值,随后只显示已有的cleaned,没有重新整理:
# 第一次:依次执行输入、整理、显示
source = " 周三开放 "
cleaned = source.strip()
print(cleaned)
# 第二次:更新输入后,只重新显示旧的整理结果
source = " 周四开放 "
print(cleaned)
# 第三次:依据当前输入重新整理,再显示
cleaned = source.strip()
print(cleaned)
三次输出依次为周三开放、周三开放、周四开放。第二次输入确实变了,但产生cleaned的语句没有重新执行,所以显示仍取旧结果。程序并没有读错source,而是这次根本没有按新source重新生成cleaned。
还可以把变化再提前一步:只在编辑区域把周三改成周四,但没有运行输入片段。此时可见的代码文字已经改变,内存里的source可能仍是周三。随后即使重新运行整理,也会使用内存中的旧输入。要说明当前结果的来源,就应区分编辑文本、运行输入、运行处理和查看输出这几个动作。
这就是为什么给AI看一张包含最新代码和旧输出的截图,仍可能不足以解释问题。截图的文字顺序不能保证对应实际执行顺序。提供这次执行过的片段、关键输入的当前值,以及输出出现的时点,才便于还原过程。修改文件不等于正在运行的进程已经加载了它,更新提示词也不等于旧结果自动按新要求重做。
在全新环境里运行完整例子,可以帮助检查程序是否依赖某个早先留下的名字。比如只运行print(cleaned),若这个环境从未建立cleaned,会出现名字未定义错误;在旧环境里却可能打印出很早以前的结果。前者暴露缺少初始化,后者则可能把问题暂时藏起来。
重新开始并不等于删除所有资料。进程内存、代码文件、已保存的输入和结果是不同对象。一个进程结束后,另一个进程要显式取得所需数据;已经正确写到磁盘的文件也不会仅因原进程结束就自动变回旧内容。要保留哪些、重新载入哪些,应依据实际保存方式说明。
使用AI协助编程时,要求它写清一段示例从什么起点运行,比笼统要求“代码完整”更有效。读者也可以用同样方式检查教程:这段建立了哪些名字,哪些是前面延续的,哪些只是讲解中假设已有。合理的连续示例可以依赖前文,但这种依赖应当让人看得见。
十一、让完成标记对应正在处理的那份输入
现在重新回到第五节完成第一轮之后的主例:T11、T13已经整理,cards中有它们的结果;T12待补充,没有结果。以下完整重建这个已经取得的状态,然后补上T12原文,按同一规则接续:
jobs = [
{"id": "T11", "source": " 借用规则 ", "state": "已整理"},
{"id": "T12", "source": None, "state": "待补充"},
{"id": "T13", "source": " 开放时间 ", "state": "已整理"},
]
cards = {"T11": "借用规则", "T13": "开放时间"}
jobs[1]["source"] = " 归还时间 "
for job in jobs:
if job["state"] not in ["待处理", "待补充"]:
continue
if job["source"] is None:
job["state"] = "待补充"
continue
job["state"] = "整理中"
cards[job["id"]] = job["source"].strip()
job["state"] = "已整理"
print(cards)
print([job["state"] for job in jobs])
结果表保留T11、T13,再加入T12的归还时间,三条状态都成为已整理。T11与T13这次被跳过,T12经过实际清理与写入完成接续。若再保持输入不变运行这段处理循环,已整理条目都会跳过;若从代码最前面的重建状态开始运行,则是在重新建立这个教学起点,两种运行范围需要分清。
现在再改变一个条件:T11的原文由借用规则改成" 新借用规则 ",但它的已整理标记保持原样。按照现有入口条件,T11仍会被跳过,旧cards内容不会自动变化。程序正确执行了写出的条件,却不满足“结果应对应最新原文”的新要求。
小叶需要把有效原文修改与重新处理的安排连起来,例如在改T11原文时把它设为待处理,再运行相应处理。这样新原文成为下一轮输入,结果写入后再标结束,旧结果就被当前结果替换。这项动作改变了造成问题的条件;只重新打印cards或把已整理改写得更醒目,都没有解决输入与结果脱节。
如果未来需要同时保留旧原文、旧结果与新结果,就不能简单覆盖全部历史。可以让每份结果带上所用原文的版本标识,或分别保存每一版。此时“已整理”应当针对某一版输入而言,而不是对这个编号永远有效。版本记录的具体工具后面再讲,这里的关系已经能够通过一个改原文的小例理解。
学完本章,小叶面对一次结果异常,可以从三个相连的问题进入:开始时实际有什么,真正执行了哪些动作,结束或中断后留下什么。判断循环是否运行、语句是否正确,只是其中的一部分;还要让当前状态、记录时点和任务要求互相对应。
本章最终完成的是三条原文的首尾空白整理,结果留在内存表中。后续若要审核事实、保存文件或发布内容,还需要安排相应步骤。这次接续之所以成立,是因为补上了T12缺失的输入,并实际走完尚未完成的处理;T11的变化则说明,完成记录还必须与所依据的输入保持联系。下一章将把这样的处理步骤封装成函数,进一步说明输入怎样交入、结果怎样交出。
练习与参考分析
练习一:同样看到一个结果,是否代表同一现场
两次独立运行都只在cards里留下T11。第一次程序在准备处理T12之前被暂停;第二次T12已经进入整理中,并在取得整理文本时出错。小叶认为结果表一样,所以恢复方式也应完全一样。这个结论是否充分?至少还要知道哪些信息?
**参考分析:**结果表相同不等于完整状态相同。第一种还没开始T12,第二种已经把T12改为整理中;若接续入口只接受待处理和待补充,第二种的整理中会被跳过。还需知道T12当前状态、原文是否取得、错误之前实际完成了哪些动作,以及入口规则接受什么情况。
在本章这个只清理内存文本的例子里,确认T12无原文且无结果后,可以将它归为待补充。若原文已经补齐,再安排清理与写入。不能只因两张结果表看起来一样,就省掉对阶段和输入的检查;也无需据此假定一定存在外部发送或复杂事务。
练习二:保存过的判断为什么仍然为真
独立记录开始为待处理,小叶保存was_pending = job["state"] == "待处理"。随后这条任务实际完成并改为已整理。此时was_pending仍为True,是否应把它改成False?若它分别用于“统计开始时有多少待处理”和“决定现在是否还要处理”,答案有什么不同?
**参考分析:**这个值记录的是保存判断时的情况,保留True本身没有错误。若用于统计开始时的待处理任务,应保留原事实,不能因为后来完成就把当初的记录抹掉。若用于决定现在是否再次处理,则应根据当前状态重新判断,而不是继续把旧值当成现在的资格。
问题在使用时点与含义是否匹配,不在布尔值有没有自动更新。把名字取成was_pending更能提示过去,但正确语义仍取决于程序何时求值、以后怎样使用;一个名称不能替代真实的更新或重新判断。
练习三:改了输入文字,为什么仍得到旧结果
小叶先依次执行输入、去空白和打印,得到“周五领取”。之后他在编辑区把输入改成“周六领取”,但没有运行输入片段,直接重新运行去空白与打印。假定仍在同一环境,之前source的值还保留着,此时会显示哪一天?要得到周六,最少还缺什么执行关系?
**参考分析:**仍会显示周五。编辑区的文字改变没有更新当前内存中的source;重新清理时读到的仍是先前source。需要实际执行新输入赋值,再依据这个新值重新清理并显示。只运行新的输入后打印旧cleaned,也仍未完成重算。
这里三个动作形成依赖:让当前输入更新、让结果根据当前输入重新生成、显示新结果。需要执行的是覆盖这条依赖链的必要部分。如果编辑的代码与实际运行的代码不是同一份,还要先确认运行入口。
练习四:历史列表为什么一起变了
独立字典只有编号和文本状态两项。小叶在待处理时执行history.append(job),完成后又执行一次相同追加。最后查看两项history,都显示已整理。他认为程序把两次状态记录成了同一时刻,是否准确?怎样保留这两个时点的字段值?
**参考分析:**两次追加确实发生在不同时间,但追加的都是同一个可变字典对象的引用。后来改状态,两处入口看到的是同一个当前对象,不能凭列表里有两项就认定保存了两个过去快照。
本题字段都是不可变文本,可以在每个所需时点追加job.copy(),让新外层字典分别保存当时的字段值。若以后加入内部可变列表,就要重新检查复制范围;这个修法的有效性来自本题数据结构,不是所有嵌套历史都只要copy一次。也应在记录中保留能区分时点或动作的信息,才能说明两份快照各在什么阶段产生。
练习五:有结果而没有完成标记,是否必须重做
在一个独立变体中,小叶已把T12的整理文本写入cards,紧接着、尚未把状态从整理中改为已整理时中断。原文没有变化,cards里的结果已经核对正确。若任务仅要求保存这份内存结果和对应状态,接续时还需要重新清理文本吗?若把清理动作换成一次外部通知发送,能否套用同一结论?
**参考分析:**按第一组明确条件,结果已经正确存在,未完成的是对应的结束标记;可以在核对后补上状态,不必重新清理。若不确定结果是否可信,才需要进一步检查或重算。不能把“整理中”机械解释成“从未得到结果”,也不能反过来只凭一个同名键存在就认定内容正确。
换成外部发送后,要另查实际发送状态。一个本地标记不能说明通知一定没发,已有可靠回执也可能支持只补记录而不重发。若没有足够证据,就保留不确定状态并选择合适核查方式;原来的纯内存文本例没有证明外部重试一定不会重复。两类任务要比较的是实际效果和可取得证据,而不是状态名称是否相同。
练习六:状态没变,是否足以证明重复操作没有额外影响
独立程序每次都把一条任务设为已整理,同时向events列表追加一个“执行了一次”的记录。连续运行两次后,任务状态与运行一次相同,但events多了一项。小叶想判断这段操作是否幂等,应先说明什么?如果这里只关心最终任务状态,可以得出什么有限结论?
**参考分析:**应先明确比较的效果范围。只看state字段,两次相同赋值留下相同状态;若把events内容也纳入结果,第二次追加改变了它,完整操作就不满足这个观察范围内的重复效果相同。不能拿最后一行状态相同,覆盖其他已经变化的数据。
若本轮约定只评价最终任务状态,可以说该字段上的设置效果重复后不变,但应保留这个范围,而不声称全部操作没有新增影响。实际系统里还可能有关联调用或费用,是否纳入要由具体任务说明;本题已经给出events,足以看清比较对象影响结论,无须先构造一个庞大的分布式系统。