跳到主要内容

运算符、表达式、条件、循环与模式匹配

本节目标

查询表达式、控制流、循环和结构模式匹配。

表达式决定得到什么对象,控制流则决定哪些表达式会继续求值、程序进入哪条路径。本章在上一章的容器、切片与复制语义之上,依次说明运算分组、分支、循环和结构模式匹配。无需先背诵整张优先级表,相关规则会随示例展开。

运算符、优先级与结合

没有括号时,优先级决定哪些部分先组成表达式;同一优先级连续出现时,再由结合方向确定分组。下表按大致从高到低列出本章常见关系,其中调用、下标和属性访问比算术运算结合得更紧。它描述的是语法分组,不能脱离具体规则机械套用,尤其要留意幂与一元运算在左右两侧并不对称。

分组关系(大致从高到低)形式说明
主表达式f(...)x[i]x.attr调用、下标与属性访问
幂与一元**+x-x~x连续幂按右侧分组;幂的右操作数可以是一元表达式,须结合下文示例理解
乘法*@///%乘、矩阵乘、除法与余数
加法与移位+-,随后 <<>>加减层与移位层各自通常从左到右分组
按位运算&,随后 ^,随后 |三层优先级,不是同一层
比较<==inis可以形成比较链
布尔运算not,随后 and,随后 ornot 高于 andand 高于 or
条件表达式a if condition else b根据条件只求值一个结果分支
lambda 与赋值表达式lambda: value,随后 name := value位于常用表达式的低优先级端

例如 total + fee * count 先组成 fee * count,而 (total + fee) * count 明确改变分组。连续的幂运算按右侧分组,所以 2 ** 3 ** 22 ** (3 ** 2) 分组。

幂与一元运算不能只靠“谁总是更高”来记:在幂的左侧,-2 ** 2-(2 ** 2) 分组,结果是 -4;在幂的右侧,2 ** -12 ** (-1) 分组,结果是 0.5。前一个表达式若想让负数成为底数,应明确写 (-2) ** 2。即使熟悉表格,只要意图不能一眼看清,也应加括号;括号既是语法分组工具,也是给读者的边界标记。

“优先级更高”不等于一条通用的“运行更早”规则:前者只描述语法分组,实际求值还受短路、条件表达式和比较链等规则控制。

比较、成员测试与身份测试

==!= 比较值是否相等,isis not 判断两个引用是否指向同一个对象。身份不是通用的值比较:字符串、数字和容器内容应按需求使用 ==,检查单例 None 通常写 value is None。不要根据某次运行中小整数或字符串似乎共享对象,就用 is 比较它们的值。

成员关系分别用 item in containeritem not in container 测试。字典默认检查键;值查询必须明确使用 mapping.values()。成员关系也不同于身份:对象可以等于容器中的某个元素,却不必与该元素是同一对象。

比较可以串联。lower < value <= upper 表示两个相邻比较都成立,效果类似 lower < value and value <= upper,但中间表达式 value 只求值一次。比较链按语言规定从左向右判断;如果前一个比较为假,后续比较所需的表达式不会求值。不要把它改写成 (lower < value) <= upper,那会先产生布尔值,再拿布尔值参与另一次比较,含义已经不同。

布尔运算与短路求值

条件位置会对对象做真值判断:FalseNone、数值零以及空字符串、空容器通常为假,其他对象通常为真。not value 总是得到布尔值;andor 则返回某个操作数,不保证返回 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

breakcontinue 的作用域仅限所在的最内层循环,不会自动退出外围循环或函数。跨越多层的退出需求应通过重新设计状态、提取函数,或在各层明确传播退出条件来表达。

循环 else 与搜索完成

forwhile 都可以带 else。这里的 else 不是“循环条件为假时每轮执行”,而是在循环正常完成后执行。for 耗尽输入或 while 条件变为假都属于正常完成,会执行 else。空的 for 输入也属于正常耗尽。breakreturn 和向外传播的异常都会跳过 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 的映射模式,并保持输出确定。

control_pattern_report.py
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 会把所有假值归为一类;若 None0 和空容器含义不同,应显式比较或分支。
  • is 比较普通值:is 判断身份,不是字符串、数字或容器内容的通用相等判断;普通值比较使用 ==
  • 忘记 andor 返回操作数:需要严格布尔结果时用清楚的比较或 bool(...),不要假定结果一定是 TrueFalse
  • while 的某条路径上不更新状态:尤其要检查 continue 之前是否已经推进条件,否则可能死循环。
  • 把循环 else 当成普通 ifelse:它表示循环正常完成,包括空输入或条件自然变假;breakreturn 或向外传播的异常会跳过它。
  • 把模式中的裸名称当成常量:裸名称会捕获并绑定;字面量或点号限定的值模式才表达相应比较意图。
  • 忽视 case 顺序和 guard 时机:分支从上到下尝试,guard 只在模式成功后运行,宽泛模式过早出现会遮住后续分支。
  • 依赖 zip 默默截断:默认 zip 以最短输入为准;长度必须一致时显式使用 strict=True

本章只讨论普通表达式、同步条件与循环、以及结构模式匹配;推导式和异步控制流不在本章范围内。继续阅读函数、参数、作用域与闭包,学习如何把这些分支与循环组织成可复用的函数边界。