运算符、表达式、条件、循环与模式匹配
本节目标
查询表达式、控制流、循环和结构模式匹配。
表达式决定得到什么对象,控制流则决定哪些表达式会继续求值、程序进入哪条路径。本章在上一章的容器、切片与复制语义之上,依次说明运算分组、分支、循环和结构模式匹配。无需先背诵整张优先级表,相关规则会随示例展开。
运算符、优先级与结合
没有括号时,优先级决定哪些部分先组成表达式;同一优先级连续出现时,再由结合方向确定分组。下表按大致从高到低列出本章常见关系,其中调用、下标和属性访问比算术运算结合得更紧。它描述的是语法分组,不能脱离具体规则机械套用,尤其要留意幂与一元运算在左右两侧并不对称。
| 分组关系(大致从高到低) | 形式 | 说明 |
|---|---|---|
| 主表达式 | f(...)、x[i]、x.attr | 调用、下标与属性访问 |
| 幂与一元 | **;+x、-x、~x | 连续幂按右侧分组;幂的右操作数可以是一元表达式,须结合下文示例理解 |
| 乘法 | *、@、/、//、% | 乘、矩阵乘、除法与余数 |
| 加法与移位 | +、-,随后 <<、>> | 加减层与移位层各自通常从左到右分组 |
| 按位运算 | &,随后 ^,随后 | | 三层优先级,不是同一层 |
| 比较 | <、==、in、is 等 | 可以形成比较链 |
| 布尔运算 | not,随后 and,随后 or | not 高于 and,and 高于 or |
| 条件表达式 | a if condition else b | 根据条件只求值一个结果分支 |
lambda 与赋值表达式 | lambda: value,随后 name := value | 位于常用表达式的低优先级端 |
例如 total + fee * count 先组成 fee * count,而 (total + fee) * count 明确改变分组。连续的幂运算按右侧分组,所以 2 ** 3 ** 2 按 2 ** (3 ** 2) 分组。
幂与一元运算不能只靠“谁总是更高”来记:在幂的左侧,-2 ** 2 按 -(2 ** 2) 分组,结果是 -4;在幂的右侧,2 ** -1 按 2 ** (-1) 分组,结果是 0.5。前一个表达式若想让负数成为底数,应明确写 (-2) ** 2。即使熟悉表格,只要意图不能一眼看清,也应加括号;括号既是语法分组工具,也是给读者的边界标记。
“优先级更高”不等于一条通用的“运行更早”规则:前者只描述语法分组,实际求值还受短路、条件表达式和比较链等规则控制。
比较、成员测试与身份测试
== 与 != 比较值是否相等,is 与 is not 判断两个引用是否指向同一个对象。身份不是通用的值比较:字符串、数字和容器内容应按需求使用 ==,检查单例 None 通常写 value is None。不要根据某次运行中小整数或字符串似乎共享对象,就用 is 比较它们的值。
成员关系分别用 item in container 和 item not in container 测试。字典默认检查键;值查询必须明确使用 mapping.values()。成员关系也不同于身份:对象可以等于容器中的某个元素,却不必与该元素是同一对象。
比较可以串联。lower < value <= upper 表示两个相邻比较都成立,效果类似 lower < value and value <= upper,但中间表达式 value 只求值一次。比较链按语言规定从左向右判断;如果前一个比较为假,后续比较所需的表达式不会求值。不要把它改写成 (lower < value) <= upper,那会先产生布尔值,再拿布尔值参与另一次比较,含义已经不同。
布尔运算与短路求值
条件位置会对对象做真值判断:False、None、数值零以及空字符串、空容器通常为假,其他对象通常为真。not value 总是得到布尔值;and 和 or 则返回某个操作数,不保证返回 bool。
left and right先求值left;若它为假,直接返回left,否则才求值并返回right。left or right先求值left;若它为真,直接返回left,否则才求值并返回right。
基于这些返回规则,name or "anonymous" 能选择后备值,items and items[0] 也能在容器为空时避开下标访问。代价是所有假值都会被归入同一类;如果 0、空字符串和“缺失”含义不同,就应改用显式条件。短路还可保护后续操作,例如 denominator != 0 and numerator / denominator > 1。不过,将带副作用的调用藏在右操作数中会让执行路径难以辨认。
if、elif、else 与条件表达式
if 根据真值进入一个缩进代码块;后续 elif 只在之前条件都为假时依次检查,最后的 else 在没有条件成立时执行。一个分支链最多执行其中一个分支,所以应把更具体的条件放在可能包含它的宽泛条件之前。
if score >= 90:
level = "excellent"
elif score >= 60:
level = "passed"
else:
level = "retry"
条件表达式 value_if_true if condition else value_if_false 在两个值之间选择,并且只求值被选中的结果表达式。简单赋值可写成 label = "adult" if age >= 18 else "minor";一旦包含多步操作或嵌套选择,普通 if 更容易读清路径。
代表脚本中的 if person["age"] >= 18 位于 for 内,每遇到一个成年人就把计数加一。缩进决定语句属于哪个分支或循环;不要靠视觉对齐猜测,应统一使用四个空格并让块边界保持清晰。
for、range、enumerate 与 zip
for target in iterable 从可迭代对象逐项取得值并绑定给目标。只需要元素时直接遍历元素,不必手工维护下标。需要一段整数时使用 range(stop) 或 range(start, stop, step);终点不包含在内,步长不能为零。
for name in names:
print(name)
for attempt in range(1, 4):
print(attempt)
需要“位置和值”时用 enumerate(iterable, start=0),例如 for index, name in enumerate(names, start=1)。需要并行取得多组元素时用 zip(left, right);默认在最短的输入耗尽时结束。如果长度不一致应视为错误,可用 zip(left, right, strict=True) 让不等长输入抛出 ValueError。
循环目标每轮都会重新绑定,循环结束后名称通常保留最后一次绑定。输入若一开始为空,循环体不会执行,该循环也不会产生新绑定。只需元素时直接遍历即可;只有任务确实依赖下标或要按位置更新序列,才需要 range(len(items))。
while、break 与 continue
while condition 在每轮开始前检查条件,并在条件为真时继续,适合终止点由运行状态决定而非遍历现有元素的任务。循环体若不能让状态朝终止条件推进,就可能永远无法退出。
break 立即退出当前最内层循环;continue 跳过本轮剩余语句,回到下一轮条件检查或取下一个元素。代表脚本从下标零开始检查列表,找到第一个偶数时保存它并 break;未找到时才递增下标。若把 continue 放在状态更新之前,就可能反复处理同一状态。
index = 0
while index < len(values):
if values[index] < 0:
index += 1
continue
process(values[index])
index += 1
break 与 continue 的作用域仅限所在的最内层循环,不会自动退出外围循环或函数。跨越多层的退出需求应通过重新设计状态、提取函数,或在各层明确传播退出条件来表达。
循环 else 与搜索完成
for 和 while 都可以带 else。这里的 else 不是“循环条件为假时每轮执行”,而是在循环正常完成后执行。for 耗尽输入或 while 条件变为假都属于正常完成,会执行 else。空的 for 输入也属于正常耗尽。break、return 和向外传播的异常都会跳过 else;单独的 continue 不会。
for language in languages:
if language == target:
result = f"found:{language}"
break
else:
result = "missing"
搜索是循环 else 的典型用途:循环体处理“找到并提前结束”,else 处理“完整检查后仍未找到”,不再需要额外布尔标记。continue 只跳过当前一轮,循环随后若正常完成,else 仍会执行。
match、case、模式与 guard
match subject 只求值一次主题,随后从上到下尝试 case。模式描述数据必须满足的形状,不等同于一串 == 判断。首个匹配成功且 guard(若有)为真的分支执行后,余下 case 不再尝试,因此宽泛模式之前应先放更具体的模式。
常用结构模式包括:
- 字面量模式,如
case 0:或case "quit":,匹配指定的字面值; - 序列模式,如
case [first, second]:,匹配并拆出两个位置; - 映射模式,如
case {"role": "admin", "action": action}:,要求指定键并把对应值继续匹配,额外键不妨碍匹配; - 捕获模式,如
case value:,把主题绑定到新名称;裸名称在模式里是捕获而不是与同名变量比较; - 通配符模式
case _:,匹配剩余情况但不绑定名称; - OR 模式,如
case "start" | "resume":,任一备选模式成功即可,各备选项必须绑定同一组名称。
与命名常量比较时,应使用合适的字面量或点号限定的值模式,例如 case Status.READY:;裸名称会执行捕获,而不会读取同名外部变量。无 guard 的捕获模式匹配所有剩余输入,其后不能再出现可到达的 case。
guard 写在模式后的 if 中。只有模式先成功,guard 才会求值;guard 为假时会继续尝试后面的 case。这意味着 guard 的副作用、分支先后顺序和捕获名称都可能影响可读性,最好让 guard 保持简短且无副作用。代表脚本先以映射模式确认角色并捕获 action,再用 guard 限定允许的动作。
固定输入让代表脚本可以同时验证条件计数、while 搜索、循环 else 和带 guard 的映射模式,并保持输出确定。
people = [
{"name": "Ada", "age": 17},
{"name": "Grace", "age": 18},
{"name": "Guido", "age": 25},
]
adult_count = 0
for person in people:
if person["age"] >= 18:
adult_count += 1
values = [3, 5, 8, 10]
index = 0
first_even = None
while index < len(values):
if values[index] % 2 == 0:
first_even = values[index]
break
index += 1
target = "python"
for language in ["c", "cpp", "python"]:
if language == target:
search_result = f"found:{language}"
break
else:
search_result = "missing"
command = {"role": "admin", "action": "write"}
match command:
case {"role": "admin", "action": action} if action in {"read", "write"}:
route = f"admin-{action}"
case {"role": role}:
route = f"user-{role}"
case _:
route = "unknown"
print(f"adult-count={adult_count}")
print(f"first-even={first_even}")
print(f"search={search_result}")
print(f"route={route}")
adult-count=2
first-even=8
search=found:python
route=admin-write
主题是映射时,{"role": "admin", "action": action} 的 action 是捕获名称;它绑定主题中对应键的值,而不是拿这个值与某个既有的 action 变量比较。该模式成功后才检查集合成员关系 guard,成功时形成管理动作路由。
表达式求值与控制流检查
- 把真值与精确状态混为一谈:
if value会把所有假值归为一类;若None、0和空容器含义不同,应显式比较或分支。 - 用
is比较普通值:is判断身份,不是字符串、数字或容器内容的通用相等判断;普通值比较使用==。 - 忘记
and、or返回操作数:需要严格布尔结果时用清楚的比较或bool(...),不要假定结果一定是True或False。 - 在
while的某条路径上不更新状态:尤其要检查continue之前是否已经推进条件,否则可能死循环。 - 把循环
else当成普通if的else:它表示循环正常完成,包括空输入或条件自然变假;break、return或向外传播的异常会跳过它。 - 把模式中的裸名称当成常量:裸名称会捕获并绑定;字面量或点号限定的值模式才表达相应比较意图。
- 忽视
case顺序和 guard 时机:分支从上到下尝试,guard 只在模式成功后运行,宽泛模式过早出现会遮住后续分支。 - 依赖
zip默默截断:默认zip以最短输入为准;长度必须一致时显式使用strict=True。
本章只讨论普通表达式、同步条件与循环、以及结构模式匹配;推导式和异步控制流不在本章范围内。继续阅读函数、参数、作用域与闭包,学习如何把这些分支与循环组织成可复用的函数边界。