小叶已经把报名记录整理成了列表,每份记录也有清楚的字段。现在他希望程序生成一张额外材料备料清单:已确认参加且明确需要材料的同学进入清单;需求尚未取得的同学进入待联系名单;已经说明不需要材料的同学不应再被追问。
这些话看起来只是几条常识,写成程序时却有不少选择。一个同学满足条件以后,要执行哪几步?同一份记录能不能同时进入两个名单?某一份不符合要求,是跳过这一份,还是结束整批处理?总数应该从哪里开始累计?本章用条件与循环把这些选择表达出来。
以下是另一场活动的虚构教学数据,不沿用前章的名单总账。六份记录已经完整读入,每个编号只出现一次,分别属于不同的同学。字段也已核对:状态只可能是已确认、待确认、已取消;额外材料需求只可能是非负整数或None。数量单位为份,零表示明确不需要,None表示尚未取得数量。
| 编号 | 状态 | 额外材料需求 |
|---|---|---|
| 101 | 已确认 | 2 |
| 102 | 已确认 | 0 |
| 103 | 已确认 | 未取得 |
| 104 | 待确认 | 3 |
| 105 | 已取消 | 1 |
| 106 | 已确认 | 1 |
本轮只整理已确认者的额外材料安排。待确认和已取消的记录仍保存在原表,只是不进入本轮备料。程序生成清单,不代表材料已经准备好或已经交给同学。明确这一步做到哪里,后面的“处理完成”才有具体含义。
一、条件先回答一个清楚的问题
程序可以用一个表达式给出判断条件,据此决定是否执行某段代码。最容易理解的例子是比较:record["status"] == "已确认"问的是,当前这份记录保存的状态是否等于约定的已确认。比较结果为True或False,也就是布尔真或布尔假。
这里的判断对象是字段中保存的状态。开篇已经给定记录经过核对,因此在这个教学任务里,可以据它决定是否进入本轮安排。如果状态只是AI根据一段含糊聊天猜出来的,同样的比较仍能执行,却不能替小叶证明同学真的确认了。程序如何比较,与比较所依赖的信息是否可靠,是前后相接的两项工作。
先看一份独立记录:
record = {"id": "101", "status": "已确认", "extra_copies": 2}
if record["status"] == "已确认":
print(record["id"], "进入本轮安排")
print("本条检查结束")
程序先打印101 进入本轮安排,然后打印本条检查结束。if表示按条件选择执行,条件后面的冒号引出一组代码;向右缩进的那行属于这个条件的控制范围。最后一行恢复到外层缩进,所以只要程序正常走到这里,无论条件成立与否,都会执行它。
把这个小例的状态独立改成待确认,第一条打印就不执行,最后的检查结束仍会打印。它没有先执行再撤销,也没有打印一句“条件为假”后才跳过;它只是没有进入那组代码。读一段条件程序时,要先圈出受条件控制的动作,不能只寻找有没有出现if这个单词。
缩进因此是Python语法的一部分。假如进入安排后需要同时记录编号和增加人数,这两个动作都应放在相应条件内部。把其中一个动作退回外层,含义就变成它不再受这个条件控制。即使文件能够运行,处理规则也已经改变。通常使用四个空格表示一级缩进,关键是所属层级一致,而不是用空格让代码看上去整齐。
==与上一章的=也要分开。=把一个值赋给目标,==比较两边是否相等。在上面的if语句中,把==误写成=会导致语法错误。若另写一条赋值语句,把状态改成已确认,那是在改记录,也不是在核对原来的状态。对本章这些文本状态,使用相等比较即可;不要因为两个地方都出现“已确认”,就把记录状态的动作和核对状态的动作混在一起。
一个条件还需要有恰当的精度。“这个记录可以处理”太宽泛,可能是字段齐全、已经确认、材料已知,或已经通过别的检查。既然本轮规则是只处理已确认者,小叶就可以逐条问:“当前这份记录是否已确认?”据此写状态比较;随后再问数量是否取得。把一个模糊的“可以”拆成不同判断,能避免让某一个布尔值承担所有含义。
二、分支的顺序,决定了哪些情况会被分开
已经确认参加,并不意味着一定要准备额外材料。102明确不需要,103还没有给数量,这两种情况都不应进入正数备料清单,却需要不同的后续处理。对一份已确认记录,可以按下面的顺序判断:
record = {"id": "103", "status": "已确认", "extra_copies": None}
copies = record["extra_copies"]
if copies is None:
print("待联系", record["id"])
elif copies == 0:
print("无需额外材料", record["id"])
else:
print("准备", record["id"], copies, "份")
这里输出待联系 103。is None检查当前值是否就是表示未知状态的None;elif表示前面的条件没有成立时,再检查这一项;else处理前面条件都未成立的其余情况。一个这样的分支链从上向下判断,一旦选中一条分支,就不会继续检查后面的elif条件或执行其他分支。
本例先把未知量分出去,再把明确零分出去,剩下的才是需要备料的正整数。这一步成立,依靠开篇已核对的输入范围:数量只能是非负整数或None。若输入里还允许负数、文本或其他对象,落入else就不能自动解释为合理的正数需求。**else的意思是其余情况,不是天然的正确情况。**它究竟涵盖什么,需要结合前面排除了什么、输入原本允许什么来判断。
对102的独立同类记录,未知判断不成立,零值判断成立,于是记录无需额外材料;对101的两份需求,前两项都不成立,才走到准备分支。这三条路径分别保持了已知零、未知和正数的含义,没有为了让流程简单,把所有不能直接备料的情况写成同一个原因。
有时几个条件会同时成立,这时顺序尤其重要。另设一个按需求量显示标签的小例:五份及以上标为较多,正数但不足五份标为普通。如果先判断“是否大于零”,八份会先进入普通分支,后面的五份门槛便没有机会再判断。若希望标签表示互斥的数量区间,就应先处理较高门槛,或者直接把每个区间的上下界写清。
例如if copies >= 5先处理五份及以上,elif copies > 0再处理剩余正数。这里第二条并不是孤立地表示所有正数,而是在第一条已经失败的前提下处理正数。读懂一个elif,需要带着前面条件未成立这一事实,而不是把每一行当成互不相干的判断题。
但并非所有工作都要选唯一分支。另一个独立用途是:已确认者加入出席名单,同时数量未知者加入资料待补名单。一个已确认但数量未知的同学可以同时进入两份名单。这时应使用两个分别执行的if,因为两个动作都可能需要发生。若误写为if与elif,第一项成立就会阻止第二项检查。
所以,选择分支结构之前,先确定实际关系:是在多个类别中选一个,还是分别检查几项可能并存的任务。代码的缩进和关键字,应当把这层关系写出来。Python的条件语句说明给出了分支依次求值的规则。
三、真假值很方便,但不能替代信息的含义
Python的if不只接受显式的True和False。它也会按约定检查其他值的真假:零、None、空文本和空列表等被视为假;非空列表、非空文本以及非零数字等通常被视为真。这种真假值测试使一些常用条件很简洁,例如if waiting_ids可以表达待联系编号列表当前不为空。
然而,程序把一个值视为假,并不表示现实中的某件事没有发生。本章的零和None都能在if copies中走到假分支,但前者明确不需要,后者尚未取得数量。若这个分支准备联系同学,直接写if not copies就会把102也放进去,造成不必要的追问。若准备把它写成“无需材料”,则会把103的未知需求错误归零。
因此,if copies回答的是这个值按Python规则是否为真,并不自动回答“数量是否已经明确”。后一个问题应写成copies is not None,在本章数据约定下,它同时接受正整数和零。若问题是“是否明确需要正数份材料”,还要进一步检查数量大于零。
and、or、not可以组合条件。and用于要求两项都成立,or用于至少一项成立,not把当前真假结果反过来。对本章这样的已核对输入,明确有正数需求可以写成:
copies = None
needs_material = copies is not None and copies > 0
print(needs_material)
copies = 0
print(copies is not None and copies > 0)
copies = 2
print(copies is not None and copies > 0)
三次分别得到False、False、True。在已确认记录中,这个合并条件可以判断是否进入备料清单;前两种情况得到同样的假,并不意味着它们以后都要采取相同行动。需要区分未知与零的工作,仍应保留刚才的多分支处理。
这里还有一个执行上的细节:Python对and采用短路求值,先判断左边;左边为假时,已经足以确定整项不能成立,就不再求右边。数量为None时,先检查“不是None”会得到假,因而不会尝试None > 0。若把顺序换过来,先做这个大小比较,就会产生类型错误,后面的None检查来不及保护它。
短路不是AI自动理解了哪个条件更安全,而是语言明确规定了求值次序。可以利用这件事表达“先确认具备比较前提,再作比较”。类似地,or左边为真时,整项已成立,不再求右边;左边为假时才继续。若后面的表达式会读取文件、修改状态或调用服务,它是否真的执行,就不能只凭它写在这一行里判断。
and、or还有一个容易藏在简写里的特点:它们返回选中的原操作数,不保证总是返回布尔值。例如0 or 5得到五,None or 5也得到五。这种写法可以表达某些默认选择,但在本章里,copies or 5会把明确零和未知一起替为五,破坏原来的信息约定。若需要补默认值,应先说清准备替换哪一种状态,再选择代码。
对状态的两种允许取值,也不能写成status == "已确认" or "待确认"。右边的非空文本本身被视为真,所以即使状态是已取消,整个条件仍为真。真正要问“属于这两种状态之一”,可以写两个完整比较,或者用status in ["已确认", "待确认"]检查成员关系。表达式中的一个文本,不会自动继承前半句的比较动作。
条件变长时,括号可以让组合作用更清楚。例如要选出“已确认,而且数量未知或数量为零”的记录,可以写status == "已确认" and (copies is None or copies == 0)。括号把两种数量情况放在一起,使它们都受已确认条件约束;未确认的记录即使数量为零,也不会入选。阅读者能直接看出组合关系,不必依靠猜测优先级来理解作者意图。Python的真假值与布尔运算文档说明了上述语言约定。
四、循环把同一个处理步骤交给每条记录
为每个编号复制一份条件代码,会让规模扩大变得困难。多一份报名就要再写一组变量和分支,修改规则时也容易只改了一部分。循环解决的是重复执行的组织:描述一次处理步骤,再让程序依次把不同项目交给它。
Python的for循环从一个可逐项取得内容的对象中依次取项目。先用三个编号观察,不改变主表:
ids = ["101", "102", "103"]
for identifier in ids:
print("核对", identifier)
print("本批编号已遍历")
循环先把101交给identifier,执行一次缩进内的打印;再把102交给同一个名字,执行下一次;最后处理103。三项用尽以后,才执行外面的整批结束提示。一次取得项目并执行循环体的过程,通常称为一次迭代;这里的循环体就是受循环控制、每轮要执行的那组代码。
identifier不是同时保存三个编号,而是在每一轮指向当前取得的那个值。名字可以由作者选择,选择能说明“一项是什么”的名字有助于阅读。遍历完整记录时用record,读者就知道循环体拿到的是一份记录,而非某个位置数字。
列表本身是可迭代对象,意思是能向这类处理依次提供项目。for遍历列表时,就会按列表次序取得这些项目。若对一个字典直接写for,默认取得的是键,这与上一章list(字典)得到键列表的规则相连,不能把取得的键直接当成内层完整记录。
循环也可以执行零次。如果ids是空列表,循环体一次都不进入,外面的结束提示仍会打印。它表示这次遍历已经走到后面的代码,不证明数据来源必定已经完整,更不证明已经处理过一个人。主表的完整性是在进入本章任务前给定的,不是被for或一条打印语句自动证实的。
在循环体中给identifier重新赋一个值,也不会改写列表原位置上的编号。下一轮仍会从列表取得下一个项目,再交给这个名字。若列表项目是一份字典,通过record["status"]修改字段则可能改到原记录,因为取得的是该对象的引用。这是前章赋值与共享规则在循环里的延续。为了生成一份清单,本章无需改变原表状态。
for负责重复,if负责选择。把一个条件放进循环体,意思是对每一份当前记录重新判断;把条件放在循环外只判断一次,则会得到不同的执行安排。判断和重复不是两套互相替代的语法,它们共同决定同一段动作会为哪些记录执行。
五、累计结果要保留过去,而不是每轮重新开始
如果每轮都能找对一份需求,怎样得到整批合计?累计需要让前面已经得到的结果保留下来,再加入当前一份。先用独立的三项已知需求观察:
quantities = [2, 0, 1]
total = 0
for copies in quantities:
total = total + copies
print(total)
最后得到三。total = total + copies不是数学里要求一个数等于它自己再加另一个数,而是一个先计算、再赋值的动作:先取旧合计和本轮数量,算出新合计,再让total保存它。循环外的零给第一轮提供起点,后面每轮都接着已经积累的结果继续。
这里要看清的是,各项需求分别在什么时候进入合计。第一轮处理两份后,合计表示已处理的第一项;第二轮加入零,合计仍是两份,但第二项已经检查;第三轮再加入一份,才得到全部三项的合计。**合计没有变化,不等于这一轮没有处理记录。**人数、记录数和材料份数要分别按各自对象统计。
| 本轮处理的数量 | 进入本轮前的合计 | 本轮后合计涵盖的范围 |
|---|---|---|
| 2 | 0 | 第一项,共2份 |
| 0 | 2 | 前两项,共2份 |
| 1 | 2 | 三项,共3份 |
若把total = 0也缩进到循环内部,每一轮开始都会清掉前面的累计。最后处理一份时,它又从零开始,于是输出一,而不是三。错误不在加法,也不在for漏了一轮,而在累计起点被放到了错误的重复范围里。
同理,收集编号的列表通常也要在当前整批处理前建立一次。每找到一个符合要求的编号,就向列表追加;若在每轮开头都写selected = [],之前收集的编号会被丢下,最后只看得到末轮留下的结果。初始化放在哪里,表达的是希望结果跨多大范围积累。
有时确实需要每组重新开始。假设小叶另外处理上午场和下午场,每场都要一份各自的合计,那么每场开始时清零是合理的。要保留全天合计,则还需一个跨两场保留的累计结果。不能把“初始化永远放循环外”背成不带范围的口诀;应当问这个量要跨哪几轮保留,它代表一条记录、一场活动,还是整天。
计数也是累计,只是每次符合条件就增加一,而不是加入材料数量。数已确认者时,明确零需求的102仍然是一个已确认者;数量未知的103也一样。若只在正数备料分支里增加“确认人数”,得到的是有已知正数需求的确认者数量,无法代表全部确认人数。变量起一个大名字,不会让它自动扩大实际统计范围。
Python允许把这里的加法赋值简写成total += copies,对本例整数合计可以按同样的累计过程理解。简写能减少重复,却不能解决把起点放错、把计数条件放错或漏掉未知量的问题。先说明这个结果应该包括哪些记录,再选用自己和AI都容易读懂的写法。
六、让条件和循环共同完成一批具体工作
现在把单条判断、遍历和累计接起来。下面完整重建开篇六份记录,并在本轮开始前建立各项结果;前面的独立小例没有修改这张表。
records = [
{"id": "101", "status": "已确认", "extra_copies": 2},
{"id": "102", "status": "已确认", "extra_copies": 0},
{"id": "103", "status": "已确认", "extra_copies": None},
{"id": "104", "status": "待确认", "extra_copies": 3},
{"id": "105", "status": "已取消", "extra_copies": 1},
{"id": "106", "status": "已确认", "extra_copies": 1},
]
confirmed_count = 0
known_total = 0
packing = []
waiting_ids = []
zero_ids = []
for record in records:
if record["status"] == "已确认":
confirmed_count += 1
copies = record["extra_copies"]
if copies is None:
waiting_ids.append(record["id"])
elif copies == 0:
zero_ids.append(record["id"])
else:
packing.append({"id": record["id"], "copies": copies})
known_total += copies
print(confirmed_count)
print(known_total)
print(packing)
print(waiting_ids)
print(zero_ids)
这段程序得到四名已确认者、已知额外材料合计三份;备料清单包含101的两份和106的一份,待联系名单是103,已知零需求名单是102。这里的三份只涵盖已确认者目前明确给出的需求。103还没有数量,因此不能把它报告成已确认者的全部最终需求。
先看外层条件的作用。104虽然写着三份,但状态待确认,按照本轮规则不进入内部数量处理;105已经取消,也不进入。它们没有从原表消失,循环仍各自取得过这两份记录,只是没有为它们执行本轮备料动作。程序遍历了六份,进入确认处理的有四份,真正形成正数备料项目的有两份,这三个数量可以同时成立。
再看内部的分支链。103进入确认处理,所以确认人数增加;随后因数量为None,进入待联系名单。102也使确认人数增加,但它进入的是已知零名单,既不备额外材料,也不加入待联系名单。101和106才使已知材料合计增加。循环使相同规则反复使用,分支则让不同记录保留各自应有的结果。

图中的每条路线都从一份当前记录开始。先判断本轮是否处理它,再区分未知、零和正数。路线最终回到下一份记录,而不是某个数量为零就结束整张表。这张图表达的是单条处理的路径;整批数据怎样依次进入它,由外层for负责。
如果小叶发现结果不符合预期,可以先沿着某一份记录的路线检查。104为什么没有进入备料?因为规则只处理已确认者。103为什么没有增加材料合计?因为数量未取得。若这些理由符合本轮目标,输出就有依据;若小叶实际上希望提前为待确认者也预备材料,则要改变任务规则,而不是把现有排除误当作循环故障。
这种分解也有助于与AI核对程序。只问“这段代码对不对”,容易得到笼统评价;拿103这样一份具体记录,让AI说明它先经过哪项判断、进入哪个名单、是否影响合计,就能检查几个关键关系。但解释仍要对照实际代码,不能只接受AI重新复述一遍小叶原本想要的规则。
还有一种常见误读是,把循环外打印一条结果当成循环只做了一次。实际的工作可以发生在循环内部,结果留在列表或累计量里,最后统一显示。反过来,循环里连续打印六行,也不证明六份记录已经正确分类。显示次数与任务完成程度之间,需要有具体的对应关系。
七、跳过当前一份,与停止整批处理,是不同的选择
嵌套条件可以表达刚才的规则,但当不处理的情况较多时,代码会不断向右缩进。另一个办法是先检查不符合本轮要求的记录,遇到它就结束当前这一轮,继续下一份。Python使用continue表达这个意思。
下面另取三份只有编号和状态的记录,只演示遍历时显示已确认者,不重新计算主例的材料合计:
records = [
{"id": "101", "status": "已确认"},
{"id": "104", "status": "待确认"},
{"id": "106", "status": "已确认"},
]
for record in records:
if record["status"] != "已确认":
continue
print(record["id"])
print("遍历结束")
输出为101、106,再到遍历结束。遇到104时,continue跳过本轮余下的打印;for随后取得106,继续执行同样规则。它没有撤销此前101的处理,也没有删除104的原记录,只改变程序接下来从哪里继续。
若把这里的continue换成break,情况就不同了。break会退出它所在的那层循环,程序遇到104时直接走到循环后的结束提示,106根本没有进入这一轮遍历。因此只显示101这个编号,随后显示遍历结束。这个程序仍然可以正常运行,但不再满足“查看全部已确认记录”的要求。
何时应该使用break?目标本来只需要找到一项结果时,提前停止就可能合理。另设一个已经按优先顺序排好的编号列表,小叶只需确认某个编号是否存在;一旦找到,就不必继续搜索后面的位置。或者任务明确只取最先找到的一份符合条件的记录,得到它以后也可以退出搜索。
这与主例需要整理全部已确认者不同。若任务要求统计全部人数、累计全部已知需求,发现第一份不符合要求就退出,会让后面尚未检查的记录无故消失在结果里。选continue还是break,关键不是“哪个写法更短”,而是现在完成的是一份记录的处理,还是整个循环的目标。
break也不等于退出整个程序。退出当前循环以后,后面的代码通常仍会继续。如果外面还有另一层循环,break结束的是最内层包住它的那一层。小叶若按场次逐场整理,在某一场内部结束查找,不会因此自动停止所有场次。能否再进入下一个场次,要看外层代码的安排。
Python还允许循环带一个else。它的用途与if后面的else不同:for正常取完项目、没有通过break退出时,可以执行这部分;while在条件为假而结束、没有通过break退出时也可如此,即使第一次检查就为假。这个结构适合表示“查找过程正常走完仍未找到”,但必须留意else与哪一层循环对齐。初读一段AI代码时,把它误接到附近的if上,会完全误解何时提示未找到。
例如,用一个found变量在循环前保存“尚未找到”,找到时改为找到并break,循环后再检查found,也能表达相同的当前查找需要。两种写法各有阅读习惯,不必为了显得熟练而强用循环else。Python控制流教程介绍了break、continue及相邻的循环else规则。这里首先要保留的是结束条件的含义。
八、遍历项目与生成序号,不是同一件事
处理记录时,通常可以直接写for record in records。如果需要的是重复固定次数,或者生成一段整数编号,则可以使用range。例如range(3)依次提供零、一、二,共三个数;range(1, 4)依次提供一、二、三,也有三个数。
它采用起点包含、终点不包含的范围约定,与前章切片的边界形式相近。range(1, 3)只能给一和二,不会因为日常说“从一到三”就包含三。若小叶想打印三张依次编号的临时卡片,终点写成三便会少一次处理。
for number in range(1, 4):
print("临时卡片", number)
输出临时卡片一、二、三。这里的数字是本小例新生成的展示编号,不是主表的报名身份编号。把它保存到哪个字段、是否允许重新编号,是数据含义问题;range只负责按给定规则提供整数。
需要跳着生成时,可以给第三个参数,称为步长。range(0, 6, 2)提供零、二、四;range(3, 0, -1)提供三、二、一。起点与方向不相容时也可能没有任何项目,例如从三出发却使用默认的正步长去到零之前,就不会开始向下数。端点、方向和步长应共同表达要遍历的范围。
range得到的对象不是已经展开的普通列表。for可以逐项使用它;需要查看这段整数的全部结果时,可以临时用list(range(...))变成列表。对循环的理解不依赖“所有数字必须先放进一张大表”,重要的是这次遍历会依次取得什么。
有时小叶既需要顺序号,又需要记录本身。可以使用enumerate把遍历中的计数和项目配成一对:
ids = ["101", "102", "103"]
for number, identifier in enumerate(ids, start=1):
print(number, identifier)
三行依次为1 101、2 102、3 103。这里for后面的两个名字接住每次提供的一对值,逗号表示按位置分别接收。start=1让展示计数从一开始,方便读者查看;它不会把原列表的第一个索引从零改成一,也不会改变101这个身份编号。
若只要项目本身,直接遍历更简洁;若需要显示“这是第几项”,enumerate能同时提供计数;若确实要按位置访问或按固定次数执行,range又有自己的用途。不要看到循环就机械写range(len(records)),再到处用位置取值。选择的入口应当让正在处理的对象清楚。
还有一个容易忽略的边界:循环里的计数,计的是当前遍历的项目。若记录经过筛选,展示序号与原表位置可能已经不同;若某个编号出现多次,序号也不证明人数没有重复。前章“一项代表什么”的问题,并没有在for出现之后自动消失。
九、while依赖每轮检查,也依赖真正发生的进展
for适合已经有一组项目、准备依次处理的场景。另一类工作更自然的表述是:“只要某个条件仍成立,就继续做。”Python使用while循环。它在每轮开始前检查条件,为真则执行循环体,执行完后再回来检查;为假则不再进入。
先看一个独立备料演示:总共七份已明确的材料,每次最多处理三份,最后不足三份就按余量处理。这里“处理”只用打印和减少待处理量来模拟,不声称已经进行实体装袋。
remaining = 7
rounds = 0
while remaining > 0:
if remaining >= 3:
batch = 3
else:
batch = remaining
print("本次处理", batch, "份")
remaining -= batch
rounds += 1
print("剩余", remaining, "轮数", rounds)
本次处理量依次是三份、三份、一份,最后待处理量降到零,共用了三轮。循环能结束,不只是因为写了while remaining > 0,还因为每一轮实际让remaining减少,而且末轮只取真实余量。七份经过这些处理逐步耗尽,条件才从成立变为不成立。
如果漏掉减少remaining的语句,循环每次看到的仍是七,条件就会一直成立。把“等待任务完成”写进条件,并不会使任务自行完成。程序需要在循环内部或其他明确的过程里获得进展;条件负责检查进展是否足够,不能创造进展。
while也可能一次都不执行。若这个独立例子从零份开始,第一次检查就为假,rounds仍是零。若任务要求至少执行一次循环体内的操作,就不能假定while天然会先做一轮再判断。入口条件决定了第一次有没有机会进入,这与执行一轮后才检查的流程不同。

图中剩余量沿七、四、一、零变化。每一次回到条件之前,先发生了本轮处理与余量更新。阅读while时,找出这条回到条件的路径,检查哪些状态在途中改变,往往比逐行念出变量更有用。
这也说明continue在while里需要特别留意。它会跳过本轮余下语句,回到条件检查;如果改变进度的动作恰好被跳过,下一轮可能仍面对同一个项目。比如按一个位置变量读取需求,遇到None就continue,而位置增加放在后面,那么程序可能反复读取同一个None。修法是让这条分支也有明确的前进方式,或改用能自动取下一项的for,而不是只追加一条“请勿死循环”的提醒。
for的下一项通常由遍历对象继续提供,while则要求作者在条件和循环体之间建立可解释的进展关系。两种写法并没有谁天然更高级;面对一张有限记录表,for往往更直接;面对剩余量逐步变化的处理,while更贴近问题。若每次处理未必减少剩余量,就还要解释可能停在哪种状态,不能套用刚才七份一定归零的保证。
十、停止尝试与取得结果,应当分别记录
有些AI任务不是一遍就能获得所需内容,小叶可能安排几次尝试。循环可以组织这些尝试,但尝试次数增加不保证结果会符合要求。为了只观察执行关系,下面把每次可能取得的结果事先列在一个短列表里:前两次没有数量,第三次取得两份。这是预设的教学序列,不是真实服务调用记录。
responses = [None, None, 2]
attempts = 0
obtained = None
while obtained is None and attempts < len(responses):
obtained = responses[attempts]
attempts += 1
print(attempts, obtained)
输出3 2。循环同时要求当前还没取得数量,以及还有一个可读取的预设结果。每轮先取出当前位置的结果,再把已尝试次数增加,下一轮就会检查新状态。前两次还没有数量,因此继续尝试;第三次取得了明确的两份需求,就可以停止读取。
这段程序还有另一种停止原因。独立把responses改成[None, None, None],循环也会在三次后结束,但obtained仍是None。此时耗尽了给定尝试机会,并没有取得数量。若把两种退出都写成“需求已补齐”,就把停止条件误说成了成功条件。
独立改为[0, None, 2]时,第一轮取得零,已经是一个明确数量,应在一次后停止。正因为本例检查的是obtained is None,它不会把零当成失败继续尝试。若写成while not obtained,就可能在已经得到有效零时继续索取,重复前面把真假值当信息状态的错误。
如果流程需要向后续步骤报告结果,就应分别说明取得了什么,以及为什么停止。取得了数量可以交给数量处理;机会用尽仍未知,则应保留待处理状态,按任务安排交还人工、另行获取资料或暂缓这项工作。具体采取哪种办法由任务要求决定,while只负责按已经给出的条件继续或退出。
这个短例没有把外部变化自动塞进循环。现实里再次调用AI、读取消息或等待同学回复,都需要相应操作;把同一个旧值读三次,不会产生三份新信息。若准备让AI反复修稿,也应有可以检查的改善目标和停下的依据,而不是只让它永远评价自己“还可以更好”。循环能放大正确的处理,也能重复没有进展的步骤。
这里的两个条件各有作用:尚未取得数量,说明还需要继续;已尝试次数小于列表长度,说明还有一个结果可读。真正读取发生在满足这两个条件之后,所以不会越过列表末端。读完再推进位置,下一轮重新检查,才把边界判断与读取动作接在一起。只在某个无关位置检查过一次长度,并不能保证之后每次读取都安全。
十一、遍历中改变原列表,可能改变下一轮会看见谁
循环的规则写对,还要注意它正在遍历的容器是否同时发生变化。另设一个小例,小叶已经得到应从临时列表中移出的编号201、202。他希望保留203,便一边遍历一边删除:
ids = ["201", "202", "203"]
remove_ids = ["201", "202"]
for identifier in ids:
if identifier in remove_ids:
ids.remove(identifier)
print(ids)
结果会留下['202', '203'],202没有被移除。remove按值删除列表中找到的第一项;本例各编号唯一,所以删除201后,原来位于后面的202向前补位。遍历过程继续取后一个位置时,取得的是已经前移后的203,202便被越过了。
错误不是条件把202判断成不该删,而是202没有按小叶期待的方式进入判断。若只检查identifier in remove_ids是否写对,就找不到这个问题。容器在遍历期间的变化,影响的是后续能取得哪些项目,发生在条件判断之前。
一种更直接的安排是保留原列表,另建结果列表:
ids = ["201", "202", "203"]
remove_ids = ["201", "202"]
kept = []
for identifier in ids:
if identifier not in remove_ids:
kept.append(identifier)
print(kept)
print(ids)
kept只有203,原ids仍保存三项。每一轮都从没有被修改的原列表取编号,把需要保留的项目交给新列表,遍历对象与结果对象分开了。这里生成的是整理结果,不是已经修改所有其他地方对原列表的引用。若任务要求正式采用新列表,可以在处理结束后明确安排;不要把新结果出现自动理解为旧对象已被各处替换。
另一种办法是遍历一个外层副本,再修改原列表。它能让正在取项目的外层序列保持稳定,但前章已经解释,浅复制不一定分开内部可变记录。因此它解决的是当前例子的外层位置变化,不能被推广为所有修改都互不影响。采用哪种方式,仍要对应实际准备改什么。
也不要因此推出“循环中绝对不能改任何数据”。累计合计、向另一份结果列表追加,正是循环常做的工作;对明确需要更新的记录改字段,也可以有合理用途。需要格外检查的是:这次修改有没有改变当前遍历所依赖的项目安排,以及后续判断是否仍在使用预期数据。
十二、回到小叶的备料清单:完成的是哪一步
重新回到开篇六条完整记录,本轮规则仍是只整理已确认者。状态和数量都没有被前面的小例修改。现在可以把这次处理说清:for让六份记录依次进入检查;外层条件选出四名已确认者;内层分支把他们分成正数备料、已知零和数量待补三种情况。
小叶得到的备料项目是101两份、106一份,当前已知合计三份。102已经明确不需要额外材料,不能被当成数量缺失去催问;103的数量尚未取得,需要继续获取信息。104待确认,105已取消,按照本轮规定没有进入备料。这个结果能够支持眼前安排,也清楚留下了哪项信息还未完成。
这时可以检查几个具有不同作用的数量:六条输入都已遍历,四名已确认者被分类,两条正数备料项目共三份,一条已知零,一条待联系。正数、零和未知这三类合起来覆盖四名已确认者;原表中的另外两条则由状态规则排除。对应关系清楚,才有理由说本轮整理已经结束。
如果小叶把规则改成“今天只准备最先遇到的一个正数需求”,找到101以后break可能合理;如果仍要整理全部需求,这样停止就会漏掉106。如果改成“尚有可用机会就继续获取缺失数量”,则需要用while之类的流程组织进展,并分清取得数量和机会用尽两种结束。这些变化来自任务目标,不是关键字本身给出的答案。
使用AI生成这类程序时,小叶不必逐个背诵所有循环写法,但应能追踪一份具体记录经过什么条件,解释一个累计结果包括哪些记录,并说明循环为什么停止。找一条未知、一条明确零、一条中间不符合要求但后面仍有合格项的记录,往往比只给AI一组全都顺利的数据,更能看清它实现的规则。
条件把判断接到动作,循环把动作组织成重复过程;初始化、分支位置和退出路径又决定哪些结果能够保存下来。下一章会进一步把这些瞬间连成完整的执行过程,研究同一份程序在不同状态下为什么产生不同结果。本章先获得一项具体能力:看到程序“跑完了”以后,能继续判断它到底处理了谁、留下了什么,以及还有什么没有完成。
练习与参考分析
练习一:为什么八份需求没有被标为较多
一个独立标签规则规定,五份及以上标为较多,一到四份标为普通,零份标为无需。输入已保证是非负整数。小叶先写if copies > 0输出普通,再写elif copies >= 5输出较多,最后else输出无需。copies为八时会输出什么?怎样调整才符合规则?
**参考分析:**八大于零,第一条分支成立并执行,后面的elif不会再被检查,因此输出普通。程序执行规则没有失效,错误是把覆盖范围更宽的正数条件放在了前面,使较多分支对所有有效正数都没有机会执行。
可以先判断五份及以上,再用elif判断剩余正数,最后处理零;也可以明确普通区间为0 < copies < 5。后一写法表达零与五之间、不含两个端点的范围。两种方案都需要保留已保证非负整数的条件;若以后放宽输入类型和范围,不能仍默认else只可能是零。
练习二:未知与零为什么都变成了五
给定三份独立需求[0, None, 2]。任务要求保留明确零,只有尚未取得数量才记入待联系名单。小叶用copies or 5补值,并用if not copies决定是否联系。两种写法分别会带来什么问题?
参考分析:copies or 5依次得到五、五、二,因为零和None按真假规则都为假。它既把零需求改掉,也给未知量编出五份;题目并没有给出这样的替换依据。if not copies又会让零与None都进入联系分支,误把明确不需要的同学当作资料未补齐。
应直接按信息状态判断,例如copies is None才进入待联系;零继续作为明确数量保存。若另有真实规则要求对未知量作临时估计,也应同时保留它是估计的依据,不能把or选择的数字自动报告为已确认需求。本题没有这项规则,所以无需补一个数字来让程序显得完整。
练习三:每项都加过,最后为什么只剩四
独立需求为[1, 2, 4]。小叶在for循环的每轮开头写total = 0,随后写total += copies,循环结束打印total。为什么得到四?若需求列表为空,这种安排还有什么问题?
**参考分析:**三轮确实分别取过一、二、四,但每轮先清零,因此上一轮的结果没有被保留。最后保存的是末项四,而不是三项合计七。把total在整批循环开始前初始化为零,循环内只更新累计,就能得到七。
若空列表使循环一次都不进入,而total此前又没有建立,循环后直接读取它还会因为没有这个名字而报错;若环境里碰巧留着旧total,则可能误读旧结果。循环前初始化不仅给第一轮提供起点,也为零轮的正常情况规定结果。本题没有记录时已知数量合计可记为零,但是否已读取完整仍要由数据流程判断,不能从数值零倒推。
练习四:第三份记录为什么没有被处理
独立记录依次为301已确认、302待确认、303已确认。小叶要收集全部已确认编号,却在遇到状态不等于已确认时执行break。结果只收集301。应如何处理?若任务改为只取得最前面连续一段已确认记录,原来的break是否可能合理?
**参考分析:**原任务需要继续检查后面的303。遇到302应跳过本轮,例如continue,或用条件只对已确认者追加;break却把整个遍历终止,303尚未被取到,因而遗漏。不能把最终列表只有301解释为原数据中只有一位确认者。
改成“最前面连续一段”以后,遇到第一条待确认便意味着这一段已结束,break可得到所需前缀301。虽然输入和代码都没变,是否正确却因目标不同而变化。验证循环不能只比较某一次输出,还要说明准备收集全部合格项,还是只要一段连续前缀。
练习五:循环停止后,能否报告已经取得数量
沿用正文的预设结果读取程序,分别令responses为[None, 0, 5]、[None, None]和空列表。每种情况会尝试几次,obtained是什么?哪些已经取得数量,哪些只是不能继续读取?
**参考分析:**第一种尝试两次,先未知后得到零;零是明确数量,循环停止,第三项五不会读取。第二种尝试两次,obtained仍为None,因为预设结果已用尽而停止。第三种一轮也不进入,attempts为零,obtained仍为None,次数小于长度的条件在起点就不成立。
三种情况都能正常到达循环后的代码,只有第一种取得数量。因此报告应包括结果状态,不能把“退出了循环”统一改写为“任务成功”。这里的尝试次数是读取预设结果的次数,不是现实中已经向同学发送了几条消息;真实流程还需有相应动作,不能从模拟循环自动推得。
练习六:保留名单正确,原名单是否已经改变
独立列表ids = ["401", "402", "403"]中,只想保留401和403。小叶遍历ids,把要保留的编号加入新列表kept,最终kept符合要求。他的同学却仍通过原ids查看到三项。谁的观察有问题?如果下一步采用kept,还需要说清什么?
**参考分析:**两人的观察都可能准确。循环创建了另一份结果列表,没有对原ids执行删除,因此原列表仍有三项。整理结果正确,不等于所有原数据入口已经被更新。这与前章区分新外层、原对象和名字绑定的知识相连。
如果在当前代码中执行ids = kept,只是让ids这个名字以后指向新列表;其他仍持有旧列表的入口不会因此自动改到新列表。若只需要向下一步传递筛选结果,直接使用kept即可;若要求某份共享的原名单本身改变,应另行明确更新范围和引用关系。题目没有给出所有入口必须同步的要求,不能为解决一个筛选任务先假定要重建完整系统。