写到这里,你已经知道 Python 能计算数值、拼接字符串,也能把对象绑定到变量名上。可一个真正能干活的程序,绝不会只从第一行机械地跑到最后一行。登录程序要判断密码对不对,点餐程序要反复接收菜品,搜索程序找到目标后要马上停下,读取文件失败时还得给用户一个能看懂的提示。把这些动作组织起来的,就是语句与流程控制。
这一章我们不把 if、for、try 当成一张需要背诵的关键字清单。我们会一直追问三个更实用的问题:解释器下一步会走到哪里,哪些代码这一次不会执行,什么条件能让流程停下来。只要你能沿着这三问把程序在纸上跑一遍,大多数缩进错误、无限循环和分支遗漏都会在真正运行前暴露出来。

程序不会凭空跳转:顺序、分支、循环和异常处理共同决定下一步去哪。
本章示例默认使用 Python 3。match 语句从 Python 3.10 开始可用;如果你的环境更旧,可以先用 if、elif、else 完成同样的判断,再升级环境。
刚开始写 Python 时,一个很常见的误会是:每换一行就是一条语句,每条语句也都只能占一行。多数时候看起来确实如此,但语言真正区分的是“逻辑行”。一条简单语句可以写在一行里,一条较长语句也可以放进圆括号、方括号或花括号后跨越多行。
# 三条简单语句
name = "小林"
score = 86
print(name, score)
# 仍然是一条赋值语句,只是为了可读性分成了多行
qualified = (
score >= 60
and name != ""
)简单语句通常完成一个直接动作,例如计算一个表达式、赋值、导入模块、pass、break、continue 或 raise。复合语句则会管理一组别的语句,if、while、for、try 和 match 都属于这一类。复合语句的句头以冒号结尾,下面缩进的一组语句叫作代码块,也可以理解为“受这个句头控制的动作集合”。
temperature = 31
if temperature >= 30: # 句头,以冒号结束
print("气温较高") # if 代码块
print("出门记得带水") # 仍在 if 代码块中
print("天气检查结束") # 已经退回顶格,不属于 if这里最值得盯住的不是冒号,而是第三个 print() 已经退回顶格。条件为假时,前两个输出都被跳过,最后一句照样执行。Python 用缩进表达归属关系,缩进不是给人看的装饰,而是语法本身。
语言允许每层使用任意一致数量的空格,但日常代码统一使用四个空格。Tab 和空格看起来可能一样,解释器却可能把它们当成不同缩进。编辑器如果没有自动转换,复制代码后就容易出现 TabError 或 IndentationError。

冒号只是打开代码块,真正把语句归到同一组的是一致的缩进。
# 错误:第二行没有进入 if 代码块
if is_member:
print("享受会员价")
# 错误:看起来想放在同一块,实际缩进层级不同
if is_member:
price *= 0.9
print(price)你也许见过 if ready: print("开始") 这种单行写法,甚至见过用分号把多个简单语句塞进一行。它们在语法允许的范围内可以运行,却会把代码块的边界藏起来。教学代码和业务代码都更适合一行一个动作、一个层级四个空格。代码短不是目标,读者能一眼确认“谁受谁控制”才是。
遇到 SyntaxError 时,箭头指向的是解释器最终发现问题的位置,不保证就是问题起点。比如复合语句漏写冒号,箭头可能指到下一行。先回看当前行和上一行的冒号、括号与缩进,比只盯着箭头更有效。
“简单”说的是语法形态,不代表功能不重要。下面这些动作都可以构成简单语句:
print("你好") 是表达式语句。函数调用先被求值,调用过程产生输出。total = 20 是赋值语句,把名字绑定到对象。import math 是导入语句,加载模块并绑定名字。raise ValueError("数量无效") 主动触发异常。break 和 continue 改变所在循环的下一步。pass 完成一次什么都不做的空操作。return 结束当前函数并把结果交给调用方,只能出现在函数体内。同一行理论上能用分号隔开几条简单语句:
name = "小余"; score = 90; print(name, score)解释器能运行,不等于适合保留。三条动作挤在一起后,断点不容易单独下,版本对比不容易看,条件句后面的归属也容易误读。拆成三行几乎总是更清楚。
复合语句内部还能继续放复合语句,于是形成嵌套。下面的第二个 if 只在用户已登录时才有机会检查:
if logged_in:
if is_admin:
print("打开管理面板")
else:
print("打开个人主页")
else:
print("请先登录")沿执行路径读这段代码时,先看外层条件。logged_in 为假,内部的 is_admin 根本不会被读取。这个“没走到就不会执行”的事实很重要:函数调用、文件读取和可能触发异常的表达式,也会因为分支没命中而完全不发生。
一条长条件可以在括号内自然换行,不需要在行尾堆反斜杠:
can_submit = (
title != ""
and content != ""
and word_count <= 2000
)圆括号没有改变布尔逻辑,只是告诉解释器这些物理行属于同一个表达式。忘记右括号时,错误常常到后面几行才被发现。看到编辑器把大片代码标成同一种颜色,或解释器提示“括号没有闭合”,要从当前行向上找最近的左括号。
初学时把变量想成盒子能应付简单例子,但它很快会让你在列表共享和解包上犯迷糊。更贴近 Python 的说法是:赋值把一个名字绑定到一个对象。解释器先完整计算等号右边,再处理左边的绑定目标。
unit_price = 12
count = 3
total = unit_price * count + 5执行第三行时,Python 先找到 unit_price 和 count 指向的对象,算出右侧结果 41,最后让 total 指向这个结果。重新执行 total = 50,只是让 total 改为绑定另一个对象,并不是把原来的整数 41 改造成了 50。
a = b = []
a.append("Python")
print(a) # ['Python']
print(b) # ['Python']右侧的空列表只创建一次,a 和 b 都绑定到同一个列表。列表可以原地修改,所以通过 a 添加元素后,从 b 也能看到。若你真正想要两个互不影响的空列表,就分别创建:
a = []
b = []对整数、字符串这样的不可变对象,x = y = 0 通常不会带来同类问题,因为后续的 x += 1 会让 x 绑定到新的整数,不能在原地把整数 0 改掉。不过,理解“是否共享”和“对象能否原地改变”仍比记住几个例外靠谱。
student = ("小周", 19, "数据分析")
name, age, direction = student
first, *middle, last = [10, 20, 30, 40, 50]
print(first) # 10
print(middle) # [20, 30, 40]
print(last) # 50没有星号目标时,左边目标数量必须与右边元素数量相同,少一个或多一个都会触发 ValueError。一个解包层级最多放一个带星号的目标,它会接住中间剩余元素,并得到一个列表。星号目标也可以放在开头或结尾。
head, *tail = "Python"
print(head) # P
print(tail) # ['y', 't', 'h', 'o', 'n']交换变量不需要临时变量,也是因为右侧先整体求值:
left = "红队"
right = "蓝队"
left, right = right, left执行到最后一行时,Python 先取得旧的 right 和旧的 left,再依次绑定给左侧目标,因此不会出现“第一个赋值覆盖了第二个还要用的值”。
count += 1 在整数场景下可以先理解为 count = count + 1。但对可变对象,增强赋值可能原地修改对象;普通加法再赋值则通常创建新对象。这个差别会被共享引用看见。
original = [1, 2]
alias = original
original += [3]
print(original) # [1, 2, 3]
print(alias) # [1, 2, 3]因此,看到 += 时别只理解成“少写几个字符”,还要看看左侧对象是否可变、是否被别的名字引用。
赋值目标还可以是对象属性、列表位置或字典键。它们的共同点是:右侧先求值,然后由目标对象决定怎样保存。
scores = [76, 81, 90]
scores[0] = 80
student = {"name": "小陆"}
student["score"] = scores[0]
class Profile:
pass
profile = Profile()
profile.nickname = "山雀"scores[0] = 80 没有让名字 scores 改绑到新列表,而是让这个列表对象修改第 0 个位置。student["score"] = ... 同理。下标不存在是否报错,取决于对象类型:给字典新键赋值会增加键值对,给列表越界位置赋值会触发 IndexError。
解包同样可以嵌套,但左侧结构必须与右侧结构对应:
record = ("小秦", (88, 92))
name, (written, interview) = record如果数据来自用户、文件或接口,直接深层解包之前要确认格式稳定。否则一个缺字段的记录就会让整个循环停在 ValueError。更稳妥的做法是先验证长度和类型,或者在结构较复杂时用 match 明确表达允许的形状。
name, _, score = ("小秦", "临时备注", 88)
print(_) # 仍然可以打印“临时备注”在普通赋值里,_ 只是合法变量名。程序员常用它表示“我不打算使用这个值”,Python 并没有因此取消绑定。一次解包需要忽略多个位置时,重复 _ 会不断覆盖同一个名字,通常没有问题;但如果后面又依赖 _ 的值,代码就会变得很难读。
if 最容易写出的样子是“条件成立就做事”,真正需要练的是分支顺序。Python 从上到下检查 if 和各个 elif 的条件,遇到第一个真值就执行对应代码块,后面的条件不再检查。若前面都不成立,才会进入 else。
score = 86
if score >= 90:
level = "优秀"
elif score >= 80:
level = "良好"
elif score >= 60:
level = "合格"
else:
level = "需要补考"
print(level) # 良好为什么要从高分往低分写?因为 86 >= 60 也是真的。如果把“60 分及以上”放在最前面,86 会立刻命中这个宽条件,后面的“80 分及以上”永远没有机会。这类问题不报错,输出看着还有点合理,所以比语法错误更难发现。

条件会从上往下检查;第一个成立的分支执行后,后面的条件就不再检查。
条件表达式最终会按真值判断。数值 0、空字符串、空列表、空字典、空集合和 None 都是假值;大多数非空对象是真值。
cart = []
if cart:
print(f"购物车有 {len(cart)} 件商品")
else:
print("购物车还是空的")这种写法比 if len(cart) > 0 更贴近 Python 的常用表达。不过“有值”和“值符合业务要求”不是一回事。字符串 "0" 非空,所以是真值;如果输入必须转成数字判断,就不要直接写 if user_input: 代替数值校验。
None 经常表示“尚未提供”或“没有结果”,判断它时推荐写 is None 或 is not None:
discount = None
if discount is None:
print("还没有设置折扣")
elif discount == 0:
print("已设置折扣值为 0")这两个状态在业务上可能完全不同。只写 if not discount 会把 None 和数值 0 都归进同一条路径。真值判断很方便,但必须先确认你是否真的想把所有假值当成一种情况。
and 从左到右检查,遇到假值就能确定整个结果,不再计算右侧;or 遇到真值后也会停止。这个机制既能保护有风险的操作,也可能让你以为会执行的函数根本没有执行。
text = ""
if text and text[0] == "#":
print("这是标题")text 为空时,第一项已经是假值,所以不会访问 text[0],自然也不会触发 IndexError。保护条件必须写在前面,反过来就失去保护作用。
user = None
if user is not None and user["active"]:
print("账户可用")不要把必须发生的副作用藏在短路表达式右侧,比如 ready and save()。它虽然可能运行,但读者不容易看出 save() 是否是主动作。用正常的 if 代码块表达更清楚。
if 0 <= score <= 100:
print("分数在合法范围内")这种链式比较相当于同时检查下界和上界,而且中间表达式只求值一次。它很适合范围判断。测试时至少覆盖下界前一个值、下界、区间内部、上界和上界后一个值。大量分支问题不是条件语法写错,而是 > 与 >= 在边界上差了一格。
if temperature >= 30:
tip = "注意防暑"
print(tip)温度低于 30 时,赋值语句没有执行,tip 就不存在,最后一行会触发 NameError。如果后续代码无论如何都要用这个名字,就让所有分支都给它赋值,或先提供合理默认值。
tip = "温度正常"
if temperature >= 30:
tip = "注意防暑"
print(tip)temperature = 36
if temperature >= 30:
print("高温提醒")
if temperature >= 35:
print("减少户外活动")这里两个提醒都应该出现,所以要用两个独立 if。而成绩等级只能选一个,适合 if—elif—else 链。判断标准很简单:这些结果能不能同时成立?能,就考虑独立 if;只能选一个,就考虑分支链。
代码一层套一层时,新手很容易让 else 对错 if。可以先挡掉不合法情况,再处理主流程。
age = 17
has_ticket = True
if age < 0:
print("年龄不能为负数")
elif not has_ticket:
print("请先购票")
elif age < 12:
print("需要监护人陪同")
else:
print("可以入场")检查分支覆盖时,先写出输入边界。上例至少要考虑负数、0、11、12,以及有票和无票两种状态。
再按代码顺序逐项判断,不要直接跳到你以为会命中的分支。前一个条件为真后,同一条 elif 链已经结束。
最后问“有没有值既不该进前面分支,又不该落入 else”。如果有,说明条件范围或顺序还需要调整。
else 总与同一缩进层级、尚未配对的 if 对应。代码嵌套较深时,不要靠眼睛猜;打开编辑器的缩进引导线,或者先把内部判断提取成函数。
match 看起来像别的语言里的 switch,但它的本事不只是比较常量。它会拿一个主题值依次尝试 case 模式,找到第一个匹配且守卫条件为真的分支。模式可以同时检查列表、元组、字典或对象的结构,并把结构里的部分值绑定到名字。
先看一个只比较常量的例子:
command = "保存"
match command:
case "打开":
print("准备选择文件")
case "保存" | "另存为":
print("准备写入文件")
case "退出":
print("准备关闭程序")
case _:
print("无法识别的命令")下划线 _ 是通配模式,放在最后接住前面都没匹配的值。case "保存" | "另存为" 是“或”模式,两边任意一个匹配就进入这一支。
真正能体现 match 优势的是结构判断。假设程序收到一个字典形式的请求:
request = {
"action": "add",
"items": ["键盘", "鼠标"],
"vip": True,
}
match request:
case {"action": "add", "items": [first, *rest], "vip": True}:
print(f"会员先加入 {first},其余商品:
这个模式没有要求字典只能有列出的键;它关心的是所需键是否存在、对应值是否符合模式。first、rest、item_id 和 action 都会在成功匹配时绑定。if item_id > 0 是守卫,模式先匹配成功,再检查这个额外条件。

序列模式看位置,字典模式看键名;结构对得上,模式才会命中并取出需要的部分。
# 不要以为 case target 是在和变量 target 比较
match command:
case target:
print(target)这里的裸名字 target 是捕获模式,它几乎什么都能匹配,并把主题值绑定给 target。它不是拿主题值与外部变量比较。要比较字符串或数字,请写字面量;要比较枚举值或模块常量,使用带点号的限定名称。由于无条件捕获已经接住所有值,后面再写别的 case 甚至可能直接成为语法错误。
match 的顺序模式可以拆列表和元组等受支持的序列,但不要把它与 for 的可迭代协议混为一谈。一个对象能被 for 逐项取出,不代表一定能匹配 [first, *rest]。字符串虽然能逐字符遍历,也不会按字符去匹配顺序模式。
value = "ab"
match value:
case [first, second]:
print("两个元素的序列")
case str() as text:
print(f"这是字符串:{text}")这里会进入第二个分支。str() 是类模式,先检查主题是不是字符串,再用 as text 保留完整值。
模式匹配还要注意“第一个成功分支获胜”。如果一个宽泛结构放在前面,后面更具体的结构就没有机会。
match request:
case {"action": action}:
print(f"收到操作:{action}")
case {"action": "add", "items": items}:
print(items) # 前一支已经能匹配带 action 的字典把具体模式放在前面,宽泛兜底放后面,思路与 elif 链类似。守卫也要尽量保持为没有副作用的判断;因为它只有在模式成功后才运行,若守卫内顺便修改数据,执行路径会变得不容易追踪。
如果主要任务是比较范围、组合许多布尔条件,if 往往更直接:
if 0 <= temperature < 10 and is_raining:
suggestion = "穿防水外套"如果主要任务是识别“这个数据是什么形状,并取出其中几部分”,match 会更清楚。不要为了用新语法,把两行 if 改成十几行模式;也不要用一长串索引和键检查,硬扛本来很适合结构模式的输入。
当你事先不知道要重复多少次,但知道“满足什么条件就继续”时,while 很自然。输入校验、菜单、重试和读取数据流都属于这类场景。
attempts = 0
password_ok = False
while attempts < 3 and not password_ok:
password = input("请输入密码:")
attempts += 1
if password == "py2026":
password_ok = True
else:
print(f"密码不对,还剩 {3 - attempts} 次"
读 while 时,可以固定检查三个位置:循环开始前如何初始化状态,句头每次检查什么条件,循环体里哪一句改变这个条件。少了第三项,往往就是无限循环。

初始化只做一次;每轮先检查,再执行和更新,最后回到条件继续判断。
# 错误示例:count 永远不变
count = 1
while count <= 5:
print(count)遇到意外的无限循环,先不要到处加 break。在循环体末尾输出关键状态,看看它是否真的向终止条件靠近。
count = 1
while count <= 5:
print(f"本轮开始:count={count}")
count += 1还有一种更隐蔽的无限循环:状态确实变化了,却朝反方向走。
remaining = 3
while remaining > 0:
print(remaining)
remaining += 1 # 应该是 -= 1while True 并不天然有错。菜单和事件循环常常故意使用永久为真的条件,再在明确的成功、退出或失败上限处 break。问题不是条件写了 True,而是退出路径是否清晰、可达、能够测试。
while True:
choice = input("输入 1 查看,输入 q 退出:").strip()
if choice == "1":
print("这里是内容")
elif choice == "q":
print("已退出")
break
else:
print("无法识别,请重试")有时循环次数由用户或数据决定,可以约定一个不会当作普通数据处理的值,称为哨兵值。
total = 0
count = 0
while True:
raw = input("输入金额,输入 q 结束:").strip()
if raw == "q":
break
try:
amount = float(raw)
except ValueError:
print("请输入数字或 q")
continue
total += amount
这里的 q 只负责终止,不计入数据。无效数字用 continue 回到输入位置,有效数字才更新汇总状态。把“退出”“无效”“有效”三条路径分开后,每条路径修改哪些变量都很清楚。
如果循环条件能直接表达退出规则,就不必强行用 while True。比如等待库存降到安全线,用 while stock > safe_stock 更容易从句头看出终点。选择形式时,优先让退出理由出现在读者第一眼能找到的位置。
无限循环卡住终端时,通常可以按 Ctrl+C 中断。中断只是把程序停下来,修复仍要回到状态变量:它在哪里初始化、在哪里更新、哪种输入或数据能让退出条件成立。
不少人第一次见 for 是 for i in range(10),于是以为它天生负责计数。其实 for 更像一位依次取件的工作人员:它向右侧对象要一个元素,绑定给左侧目标,执行一轮;再要下一个,直到对方明确表示已经没有元素。
for char in "猫和老鼠":
print(char)
for line in ["第一行", "第二行", "第三行"]:
print(line)字符串、列表、元组、字典、集合、打开的文件,以及许多库返回的对象,都能按这种协议交出元素,所以它们是可迭代对象。for 不要求右侧一定是列表,也不要求事先知道元素个数。
你可以先用下面这个近似过程建立直觉:Python 获取一个迭代器,反复调用“给我下一个”,取到值就执行循环体,收到“已经耗尽”的信号就结束。日常写代码不必手动调用这些底层操作,但这个直觉能解释为什么生成器和文件也能直接进入 for。
for number in range(5):
print(number) # 0、1、2、3、4
for number in range(2, 8, 2):
print(number) # 2、4、6
for number in range(5, 0, -1):
print(number) # 5、4、3、2、1range(stop) 从 0 开始;range(start, stop) 指定起点;range(start, stop, step) 再指定步长。停止值永远不包含在结果里。步长为负时,起点必须大于停止值,否则得到空范围。步长不能是 0。
range() 返回的不是提前装满所有整数的列表,而是一个能在迭代时给出所需值的范围对象。要看到全部值,可以为了观察写 list(range(5)),但循环本身无需这一步。
需要编号时,不要手动维护一个容易忘记更新的计数器,也不要默认写 range(len(items))。
tasks = ["检查输入", "计算结果", "保存文件"]
for number, task in enumerate(tasks, start=1):
print(f"{number}. {task}")enumerate() 每轮给出一个二元组,解包后分别得到计数和值。计数默认从 0 开始,展示给用户的清单通常可以设 start=1。只有当你真的需要拿索引去修改同一列表位置时,range(len(items)) 才可能更合适。
names = ["小孟", "小何", "小叶"]
scores = [88, 93, 79]
for name, score in zip(names, scores):
print(f"{name}:{score} 分")默认情况下,zip() 在最短的可迭代对象耗尽时停止。这个行为有时正合适,有时会悄悄吞掉数据。
names = ["小孟", "小何", "小叶"]
scores = [88, 93]
for name, score in zip(names, scores):
print(name, score)
# 小叶不会出现,也不会自动报错如果业务要求两边必须等长,可以在支持该参数的 Python 版本中使用 zip(names, scores, strict=True)。长度不一致会触发 ValueError,比静默漏掉一条记录安全得多。
# 容易漏删:删除后后续元素会移动位置
numbers = [1, 2, 4, 5, 6]
for number in numbers:
if number % 2 == 0:
numbers.remove(number)更稳妥的办法是构造新列表,或者遍历副本:
numbers = [1, 2, 4, 5, 6]
odd_numbers = []
for number in numbers:
if number % 2 != 0:
odd_numbers.append(number)
print(odd_numbers) # [1, 5]prices = {"面包": 8, "牛奶": 6}
for product in prices:
print(product) # 面包、牛奶
for product, price in prices.items():
print(f"{product}:{price} 元")直接遍历字典得到键,通过键可以再取值;需要两者时,.items() 配合解包更自然。现代 Python 字典会保留插入顺序,但“插入顺序”不等于“按键名排序”。若输出必须按价格或名称排列,要显式使用 sorted()。
遍历时改变现有键对应的值通常可行,增加或删除键却可能触发“字典大小在迭代期间改变”的运行时错误。可以遍历 list(prices) 得到键的快照,再修改原字典。
列表可以反复遍历,因为每次都能取得新的迭代器;文件对象和许多生成器更像一次性票据,走到结尾后不会自动倒回开头。
numbers = iter([10, 20, 30])
for number in numbers:
print(number)
for number in numbers:
print(number) # 已耗尽,没有输出如果确实需要再次遍历,应重新创建数据源、对文件执行明确的定位操作,或在数据量允许时一次性保存到列表。不要为了保险无条件 list(),因为输入可能很大,甚至可能是没有终点的数据流。
for row in range(3):
for column in range(4):
print(row, column)外层每一轮都会完整跑四轮内层,所以一共执行 12 次输出。数据一大,嵌套层数带来的增长非常明显。排查“程序怎么这么慢”时,除了看单条语句,也要算循环体究竟被执行多少次,并确认内层有没有重复做本可在循环外完成的计算。
这四个词经常同时出现,也最容易被混成“反正都是跳过”。实际上它们改变流程的方式完全不同:
break:立刻结束最内层的 for 或 while。continue:结束当前这一轮,直接开始下一轮。pass:什么也不做,只是占住语法要求必须有语句的位置。else:只有循环没有被 break 打断时才执行。
break 离开整条跑道,continue 跳到下一圈,pass 原地通过;只有正常跑完才进入循环的 else。
for row in range(3):
for column in range(3):
if column == 1:
break
print(row, column)每次内层循环运行到 column == 1 就结束,但外层的下一行仍会继续。Python 没有一个普通 break 能同时跳出所有嵌套层。复杂场景可以把搜索封装进函数后 return,或用标志变量表达“已经找到”。
records = ["85", "缺考", "91", "", "76"]
for record in records:
if not record or record == "缺考":
continue
score = int(record)
print(f"有效成绩:{score}")碰到空字符串或“缺考”时,只跳过本轮剩余代码,之后的记录照常处理。while 使用 continue 时要更谨慎:如果状态更新写在 continue 后面,那一轮永远走不到更新位置,可能形成无限循环。
def load_cache():
pass
for value in [1, 2, 3]:
if value == 2:
pass
print(value)这个循环仍会打印 1、2、3,因为 pass 执行完后接着往下走。它适合暂时留空的函数体或代码块,不适合表达“跳过这个元素”。在异常处理里随手 except: pass 更危险,因为错误会被完全藏起来,程序可能带着不完整状态继续运行。
users = ["小唐", "小夏", "小苏"]
target = "小夏"
for user in users:
if user == target:
print(f"找到了:{user}")
break
else:
print("名单里没有这个人")如果找到目标,break 会同时跳过循环的 else;如果遍历完整个名单仍没找到,循环自然耗尽,else 执行。continue 不会阻止 else,因为它没有打断循环。
空的可迭代对象一轮都不执行,却也算正常结束,所以会进入 else。while 的条件第一次检查就是假值时也是一样。
一个经典用途是判断质数。这里不需要额外维护 found_factor 标志,找到因数就 break;完整检查后都没找到,才进入 else。
number = 29
if number < 2:
print("不是质数")
else:
for factor in range(2, int(number ** 0.5) + 1):
if number % factor == 0:
print(f"不是质数,可以被 {factor}
注意两个 else 的缩进不同:外层 else 属于 if number < 2,内层 else 与 for 对齐。把这段逻辑复制进更深的嵌套前,最好先封装为 is_prime() 函数,避免光靠缩进维持复杂关系。
把循环 else 想成 no_break 会更准确:不是“循环条件为假才执行”的普通备选分支,而是“搜索过程没有被成功命中的 break 提前结束”。
语法正确不代表运行一定成功。int("十二") 会触发 ValueError,访问不存在的字典键会触发 KeyError,打开不存在的文件可能触发 FileNotFoundError。异常处理的目标不是让所有错误消失,而是只接住你知道如何处理的失败,并把程序带到一个明确状态。
raw = input("请输入人数:")
try:
count = int(raw)
except ValueError:
print("人数必须写成整数,例如 3")
else:
print(f"已记录 {count} 人")执行顺序是:先运行 try;没有异常就跳过所有 except,再进入 else;发生异常时,try 剩余部分不再执行,解释器寻找第一个能匹配异常类型的 except。处理完成后,流程走到整个结构之后。

try 先尝试;出错走 except,没出错才走 else,而 finally 无论如何都会负责收尾。
# 不够理想:转换、计算和保存全挤在 try 里
try:
price = float(raw_price)
total = price * quantity
save_order(total)
except ValueError:
print("价格格式不正确")如果 save_order() 内部也触发了 ValueError,这段代码会误报成“价格格式不正确”。更清楚的写法是只保护转换,把成功后的动作放进 else:
try:
price = float(raw_price)
except ValueError:
print("价格格式不正确")
else:
total = price * quantity
save_order(total)异常类型存在继承关系。一个宽泛处理器放在前面,会提前接走后面那些更具体的异常。
try:
with open("scores.txt", encoding="utf-8") as file:
first_line = file.readline()
score = int(first_line)
except FileNotFoundError:
print("找不到 scores.txt")
except ValueError:
print("文件第一行不是整数")
except OSError as error:
print
FileNotFoundError 属于更具体的文件错误,应该放在较宽的 OSError 前面。except Exception as error 可以在程序边界记录大多数常规异常,但不该成为每段代码的默认写法;如果你不知道怎么恢复,记录上下文后重新 raise,往往比假装成功更诚实。
connection = open_connection()
try:
send_data(connection)
finally:
connection.close()无论 send_data() 成功、触发异常,还是流程用 return 离开,finally 都会在离开 try 结构前运行。文件这类资源通常优先使用 with,因为上下文管理器已经把可靠清理表达得更清楚。不要在 finally 里随意 return,它可能覆盖原来的返回值,甚至掩盖正在传播的异常。
漏冒号、括号未闭合和不合法缩进属于语法错误。解释器要先把整个文件解析成有效结构,才有机会执行里面的 try。因此,不能在同一个存在坏语法的文件里用外层 try 把它救回来。
# 这不是能在运行到某处后才捕获的普通错误
if ready
print("开始")而 int("十二") 的语法完全合法,只在执行转换时失败,所以可以捕获对应的 ValueError。排查时先看错误发生在解析阶段还是运行阶段:前者修代码结构,后者再判断是否属于可预期、可恢复的业务失败。
try:
value = config["retry"]
retry = int(value)
except (KeyError, TypeError, ValueError) as error:
print(f"重试次数配置无效:{error}")圆括号里的异常类型共享同一套处理逻辑。只有在恢复方式相同时才适合合并;如果缺少键要使用默认值,而格式错误要提示用户修改,就应分成不同 except。
异常对象通常包含具体原因,as error 能让日志更有用,但给最终用户展示时要补上业务语境。只显示“invalid literal”往往不如“重试次数必须是整数”清楚。
尽量不要捕获 BaseException。系统退出和用户中断等信号也在它的体系下,应用通常不应该悄悄吞掉。常规兜底从 Exception 考虑,并且只放在你能记录、清理或重新抛出的边界位置。
def calculate_discount(rate):
if not 0 <= rate <= 1:
raise ValueError("折扣率必须在 0 到 1 之间")
return 100 * (1 - rate)这里没有等待 Python 自己“出错”,而是业务规则主动拒绝不合法输入。捕获到异常后单独写 raise,可以在记录信息后把当前异常继续抛给上层。
当程序长到不能全塞在一个文件里,模块帮我们分工。import 做的事情可以先理解为两步:找到并加载模块,然后在当前作用域绑定一个可用名字。
import math
radius = 3
area = math.pi * radius ** 2
print(area)这里绑定到当前作用域的是名字 math,模块里的 pi 仍通过 math.pi 访问。模块前缀看似多打几个字符,却明确说明名称来自哪里,也不容易和自己的变量冲突。
import statistics as stats
from pathlib import Path
average = stats.mean([80, 90, 100])
current_file = Path("notes.txt")as 给导入名称起别名。from pathlib import Path 会把 Path 直接绑定到当前作用域,但不会同时创建一个可用的 pathlib 名字。别名应当帮助阅读,不要把清楚的模块名缩成只有作者懂的单字母。
from module import * 会一次引入许多名字,读者难以判断一个函数来自哪里,也可能覆盖已有变量。交互式试验偶尔能用,正式代码更适合显式导入。
def circle_area(radius):
import math
return math.pi * radius ** 2
print(circle_area(2))
# print(math.pi) # NameError:函数外没有绑定 math通常把导入放在模块顶部,依赖一眼可见,也能复用已加载模块。函数内部导入有特定用途,但别把它误认为全局可见。
模块不只是装函数的盒子,它的顶层赋值、输出和函数调用都属于可执行语句。一个模块在当前解释器进程中第一次导入时会初始化,之后普通重复导入通常复用已经加载的模块对象。
假设 settings.py 写着:
print("正在初始化设置")
theme = "浅色"别的文件第一次执行 import settings 时,会看到初始化输出,并可以访问 settings.theme。这也是为什么模块顶层不宜随意发网络请求、改文件或做昂贵计算:仅仅导入一个函数,就可能触发一连串意外动作。
要让某段演示代码只在文件被直接运行时执行,而被导入时不执行,可以检查模块的 __name__:
def main():
print("运行主程序")
if __name__ == "__main__":
main()文件被直接运行时,__name__ 是 "__main__";被当作模块导入时,它通常是模块名。这个判断仍然是普通 if,没有隐藏魔法。
if True:
message = "已经准备好"
for index in range(3):
pass
print(message) # 可以访问
print(index) # 2Python 的 if、for、while 和 try 代码块不会像函数那样创建独立局部作用域。循环目标 index 在循环后仍绑定在当前作用域。不过,如果 range(0) 一轮也没有执行,而此前又没有 index,循环后访问它就会触发 NameError。不要依赖“循环最后一次留下的值”完成重要逻辑,最好在循环前显式初始化结果。
discount = 0.9
def show_discount():
print(discount) # 只读取,可以找到模块作用域的名字
def change_discount():
print(discount)
discount = 0.8 # 函数内有赋值,discount 被判定为局部名字调用 change_discount() 会触发 UnboundLocalError。解释器在分析整个函数时看见对 discount 的赋值,于是函数里的 discount 都按局部名字处理;可执行 print() 时,这个局部名字还没绑定。
确实需要修改模块级名字时可以声明 global discount,需要修改最近一层外部函数的名字时可以用 nonlocal。但频繁依赖全局可变状态会让函数难测、流程难追。多数情况下,把值作为参数传入并返回新值更清楚。
def update_discount(old_discount, new_discount):
print(f"原折扣:{old_discount}")
return new_discount
discount = update_discount(discount, 0.8)模块名不要与标准库重名。项目目录里如果有自己写的 random.py、json.py 或 statistics.py,import 可能优先找到它,导致你以为在使用标准库,实际加载了本地文件。
单独看语法不难,难的是它们一起出现时仍能说清执行路径。下面做一个简化的学习任务清单:用户可以添加任务、查看任务、完成任务和退出;输入错误不会让程序直接崩掉。
def show_tasks(tasks):
if not tasks:
print("任务清单还是空的")
return
for number, task in enumerate(tasks, start=1):
mark = "✓" if task["done"] else "·"
print(f"{number}. [{mark}] {
程序先创建空列表,再进入 while True。退出不是靠条件自然变假,而是由“退出”分支里的 break 明确完成。
每轮先取得命令,match 只执行第一个匹配分支。添加空任务时,continue 直接开始下一轮,因此不会执行该分支后续动作。
“完成”分支把可能失败的字符串转换和列表访问放进 try。只有两步都成功,else 才会修改任务状态。
这段程序还有一个容易忽略的边界:输入编号 0 时,tasks[number - 1] 会访问 tasks[-1],它不是越界,而是最后一项。业务规则需要正数,所以应在取列表元素前补一层检查。
if number < 1:
raise IndexError
task = tasks[number - 1]这就是分支覆盖测试的价值:别只试“1”和一个特别大的数字,还要试 0、负数、空字符串、空清单,以及合法编号的上下边界。
读流程控制最有效的练习,不是再看一遍定义,而是改变输入后预测下一步。下面的交互页会把条件命中、循环控制和异常去向拆成可观察状态。每次先在心里写下预测,再操作控件核对。
先调整订单金额、会员状态和地区,观察条件从上到下检查时,哪一条分支最先命中。再切换条件排列方式,比较独立 if 与 if—elif 链为何会产生不同结果。
这个演示把每一轮的当前元素、状态更新和出口画在同一条轨迹上。重点观察 continue 后哪些语句没执行、break 如何影响循环 else,以及 while 的状态怎样向终点靠近。
选择正常输入、格式错误和除零等不同场景,然后逐步执行。留意异常发生后 try 剩余部分怎样被跳过、哪一个 except 接手,以及 finally 为什么在每条路径上都会出现。
程序“没有报错但结果不对”时,先不要整体重写。把输入固定下来,按执行顺序逐步缩小范围。
检查句头依赖哪些状态,循环体是否在所有路径上更新它们。特别注意 continue 是否绕过了更新语句,以及更新方向是否与退出条件相反。调试时可以暂时加轮次上限,防止终端一直刷屏。
rounds = 0
while condition:
rounds += 1
if rounds > 1000:
raise RuntimeError("循环超过预期轮次")如果某句“总是执行”或“从不执行”,把它与上方句头的缩进列对齐检查。代码格式化工具能统一空格,却不会替你判断业务上应该属于哪个分支。修完缩进后,要重新用真假两组输入测试。
给每个 elif 准备一个只会命中它的输入。若某分支无论如何都进不去,先看前面的条件是不是已经把它包含了。数值区间通常从严格到宽松排列,也可以直接写上下界,让范围一眼可见。
if 90 <= score <= 100:
level = "优秀"
elif 60 <= score < 90:
level = "合格"
elif 0 <= score < 60:
level = "未合格"
else:
level = "无效分数"两组数据来自不同文件或不同接口时,默认 zip() 的最短停止规则可能掩盖缺失。先验证长度,或使用 strict=True 让不一致直接变成异常。
搜索 except:、except Exception 和 pass 的组合。如果处理器没有输出、记录、恢复或重新抛出,错误很可能被静默隐藏。调试阶段至少打印异常类型和关键输入,确认接住的真是你预期的错误。
删除列表元素会让后续位置前移,增加字典键可能触发运行时错误。先遍历副本,或把符合条件的元素收集到新容器。流程控制代码越复杂,越应该避免同时改变“路线”和“道路本身”。
只在脑中跟代码跑,变量一多就容易漏。最朴素也很有效的办法是画一张表:每执行一轮,只记录会影响下一步的状态、这次命中的路径和本轮结束后的值。
numbers = [4, -2, 7, 0, 5]
total = 0
count = 0
for number in numbers:
if number <= 0:
continue
total += number
count += 1
average = total / count可以按下面的顺序记录:
最后平均值是 16 / 3。这张表还能暴露另一个边界:如果所有元素都小于等于 0,count 会保持为 0,最后一行触发 ZeroDivisionError。修复不是粗暴捕获除零异常,而是明确规定“没有有效数据”时该怎样处理。
if count == 0:
print("没有可计算平均值的正数")
else:
average = total / count
print(f"平均值:{average:.2f}")这套记录法也适合 while:额外写下“条件检查前的值”和“本轮是否执行更新”。适合 try 时,则记录哪一条语句触发异常、后续哪些语句被跳过、命中哪个处理器。表不需要漂亮,只要让每一次状态变化都能对账。
一段流程控制代码至少要试四类输入。第一类是正常输入,用来确认主路径能完成。第二类是边界输入,例如空列表、0、最大允许值和刚刚越界的值。第三类是格式错误,例如需要整数却输入中文。第四类是中断路径,例如搜索在第一个元素就找到、最后一个元素才找到,以及完全找不到。
以三次密码重试为例,只测试“一次输对”远远不够。还要测试前两次错误第三次正确、三次都错误、输入两侧有空格,以及输入阶段发生用户中断。如果代码把尝试次数加在了错误位置,这几组测试很快就会暴露“少给一次机会”或“成功后仍提示锁定”的问题。
分支测试可以给每条路径准备一个代表输入,并在代码旁暂时标记路径名:
if age < 0:
result = "输入错误" # 路径 A
elif age < 12:
result = "儿童票" # 路径 B
elif age < 60:
result = "成人票" # 路径 C
else:
result = "优惠票" # 路径 D输入 -1、0、11、12、59、60,就能同时检查各段范围与切换边界。等逻辑稳定后可以删掉路径注释,测试用例则值得保留。你会发现,流程控制真正难的不是写出冒号,而是证明每一种重要情况都能走到正确出口。
先自己沿执行路径写答案,再展开参考解。不要只看最终输出,要说明为什么某一条分支被执行、另一条被跳过。
下面程序输入 95 时会输出什么?怎样修改才符合“90 分以上优秀,60 分以上合格”的要求?
score = 95
if score >= 60:
print("合格")
elif score >= 90:
print("优秀")
else:
print("未合格")for number in [2, 4, 6]:
if number % 2 == 1:
print("找到奇数")
break
else:
print("全是偶数")numbers = [3, -1, 5]
index = 0
while index < len(numbers):
if numbers[index] < 0:
continue
print(numbers[index])
index += 1学完这一章,你不需要背出所有语法定义,但应该能拿着一段代码回答:当前名字绑定到什么,哪个条件先检查,一轮循环怎样推进,什么动作会中断流程,异常发生后谁来接住。之后遇到更长的函数,也继续用这套办法,把复杂程序拆成一条条可验证的执行路径。
show_tasks() 用 if 处理空列表,用 return 结束函数;有数据时再用 enumerate() 同时得到显示编号和任务对象。