基础篇 / 第 3 章
第 3 章 · 流程控制:分支与循环
if/elif/else、for/while、range()、break/continue/else,并延伸到循环嵌套、推导式与 3.10 起的 match 模式匹配。前置知识:会赋值、会比较。第 2 章的内容是本章的地基。学完本章,你能独立写出任何分支逻辑与循环逻辑,并知道什么时候该用哪种循环。顺序结构与缩进规则
Python 代码默认自上而下顺序执行。要改变执行顺序,就必须引入分支(if)或循环(for/while),而这两者都靠缩进界定代码块。
三种基本结构
# 顺序:逐行执行
print("第一步")
print("第二步")
# 分支:根据条件选择路径
if True:
print("走了分支")
# 循环:重复执行
for i in range(3):
print(f"第 {i} 次")
1.1 缩进是语法,不是风格
在 C、Java 中缩进只影响可读性;在 Python 里缩进决定了代码块的边界,写错缩进直接改变程序逻辑甚至报语法错误。
| 规则 | 说明 |
|---|---|
| 统一 4 个空格 | 官方推荐,不要混用 Tab 和空格 |
| 多级嵌套等比递增 | 每进一层多缩进 4 个空格 |
冒号 : 表示块开始 | if x: 后面必须缩进 |
| 空行和注释不影响块 | 空行不结束代码块 |
| 块结束看缩进回退 | 不需要写 end(对比 Ruby/Python 的 end) |
缩进错误的两种典型表现
# 错误 1:缩进不一致 —— IndentationError
if True:
print("A")
print("B") # IndentationError: unexpected indent
# 错误 2:忘写冒号 —— SyntaxError
if True
print("A") # SyntaxError: expected ':'
# 正确写法:冒号 + 统一缩进
score = 85
if score >= 60:
print("及格")
if score >= 90:
print("而且是优秀") # 嵌套:再缩进一层
else:
print("不及格")
Ctrl+Shift+P → Render Whitespace: All),能一眼看出空格和 Tab 混用的问题。分支:if / elif / else
分支(conditional)用于根据条件的真假选择不同的执行路径。
2.1 最简形式
if 语句的三种形态
age = 20
# 只有 if
if age >= 18:
print("成年")
# if + else
if age >= 18:
print("成年")
else:
print("未成年")
# if + elif + else(多分支,从上到下只匹配第一个成立的)
score = 85
if score >= 90:
level = "优秀"
elif score >= 80:
level = "良好"
elif score >= 60:
level = "及格"
else:
level = "不及格"
print(level) # 良好
elif 是「else if」的缩写,但只能写在 if 之后,不能单独开头。else 最多一个,且必须放最后。2.2 条件表达式的真假规则
条件不一定要写成 x > 5 这种形式,任何能求值成布尔值的表达式都可以。第 2 章讲过的真假值规则在这里直接生效。
把真假值用进条件
# 直接判断容器非空 —— Python 最地道的写法
names = ["张三", "李四"]
if names:
print("有人") # 列表非空 -> True
if not names:
print("没人")
# 直接判断字符串非空
text = input("请输入:")
if text:
print("你输入了内容")
else:
print("输入为空")
# 直接判断 None
user = None
if user: # None 是假值
print(user)
else:
print("用户不存在")
# 链式比较(第 2 章)
age = 25
if 18 <= age <= 60:
print("成年")
# 多个条件用 and / or / not 组合
temp = 28
if temp > 30 or temp < 10:
print("温度不适合出行")
elif 15 <= temp <= 25:
print("舒适")
else:
print("还行")
# in 运算符:判断成员关系
fruit = "苹果"
if fruit in ["苹果", "香蕉", "橙子"]:
print("是水果")
if fruit not in ["石头", "桌子"]:
print("不是杂物")
if x: 判断的是「真假」而不是「是否为某个值」。写 if text == "": 不如写 if not text: 简洁且覆盖更多「空」的形态(""、None、空列表等)。2.3 嵌套分支与提前返回(卫语句)
分支可以任意嵌套,但嵌套超过 3 层就该考虑重构了。Python 常用的重构手法是提前返回(early return / guard clause)——把「出错的分支」先处理掉,让主线保持平坦。
深层嵌套 vs 卫语句
# 反面教材:嵌套三层
def process(user):
if user:
if user["age"] >= 18:
if user["id_verified"]:
if user["balance"] > 0:
return "可以下单"
return "无法下单"
# 推荐改写:把否定条件提前 return,主线一层不嵌套
def process(user):
if not user:
return "无法下单:用户不存在"
if user["age"] < 18:
return "无法下单:未成年"
if not user["id_verified"]:
return "无法下单:未实名"
if user["balance"] <= 0:
return "无法下单:余额不足"
return "可以下单"
2.4 三元表达式与 match 模式匹配
三元表达式:if-else 的紧凑版
score = 85
# 传统写法
if score >= 60:
result = "及格"
else:
result = "不及格"
# 三元表达式(条件表达式)
result = "及格" if score >= 60 else "不及格"
print(result) # 及格
# 注意:三元是「表达式」,不能当语句用,也不该滥用以免可读性下降
a, b = 5, 3
print("大" if a > b else "小") # 大
match-case:Python 3.10+ 的结构化模式匹配
"""处理不同形态的命令,比一长串 if-elif 更清晰。"""
def run(cmd: str) -> str:
parts = cmd.split()
match parts:
case ["quit"]:
return "退出程序"
case ["add", a, b]:
return f"相加结果 {int(a) + int(b)}"
case ["greet", name] if name.isalpha():
return f"你好,{name}!"
case ["greet", *_rest]: # * 匹配剩余任意多个
return "你好,陌生人"
case ["move", ("n" | "s" | "e" | "w") as direction]:
return f"向 {direction} 移动"
case _:
return f"未知命令:{cmd}"
print(run("quit")) # 退出程序
print(run("add 3 4")) # 相加结果 7
print(run("greet Bob")) # 你好,Bob!
print(run("greet 12345")) # 你好,陌生人
print(run("move n")) # 向 n 移动
print(run("foo")) # 未知命令:foo
match 是 Python 3.10 引入的新语法。它不是「C 的 switch」,而是解构匹配:可以匹配列表长度、元素类型、字典结构,比 if-elif 更有表达力。用 case _: 做兜底。match 语句里的 case 必须缩进形成代码块,语法与 if 类似;且不支持 C 那种「穿透式」的 fall through,需要多个 case 合并时用 | 或在单个 case 里写多个模式。range():循环的计数基石
for 循环最常配合 range() 使用。range(start, stop, step) 生成一个惰性序列:包含 start,不包含 stop。
| 写法 | 等价 | 生成的值 |
|---|---|---|
range(5) | range(0, 5, 1) | 0 1 2 3 4 |
range(2, 6) | start=2, stop=6 | 2 3 4 5 |
range(0, 10, 2) | step=2 | 0 2 4 6 8 |
range(5, 0, -1) | 反向 | 5 4 3 2 1 |
range(1, 2, 0.5) | 浮点步长 | 1.0 1.5 2.0… |
range 的三种参数形式
# 单参数:从 0 开始
for i in range(3):
print(i) # 0 1 2
# 双参数:从 start 到 stop(不含 stop)
for i in range(2, 6):
print(i) # 2 3 4 5
# 三参数:加上步长 step
for i in range(0, 10, 2):
print(i) # 0 2 4 6 8
# 反向遍历
for i in range(4, 0, -1):
print(i) # 4 3 2 1
# range 是惰性的:不占内存,可以遍历超大范围
total = sum(range(1, 1_000_000)) # 一次都不必放进列表
print(list(range(3))) # [0, 1, 2] 要变成列表得显式转换
print(range(5)) # range(0, 5)
print(len(range(5))) # 5
print(3 in range(5)) # True
print(2 ** 100 in range(3)) # False,范围外
range 是「左闭右开」区间,即 [start, stop)。所以 range(len(items)) 正好得到所有合法下标 0 ~ len-1,这也是 Python 里索引计数的默认约定。enumerate 同时给出「下标和值」,所以绝大多数情况下不必用 range(len(...)) 手动管理下标,这是第 5 章的重点内容。for 循环:遍历可迭代对象
for 的本质是「从一个可迭代对象(iterable)里依次取出元素,绑定到循环变量,执行循环体」。
for 遍历各种对象
# 1. range
for i in range(3):
print(i)
# 2. 字符串 —— 遍历的是「字符」
for ch in "你好":
print(ch) # 你 好
# 3. 列表
for item in [10, 20, 30]:
print(item)
# 4. 元组
for x, y in [(1, 2), (3, 4)]:
print(x, y) # 1 2 / 3 4
# 5. 字典 —— 默认遍历「键」
user = {"name": "张三", "age": 18}
for key in user:
print(key) # name / age
for key, value in user.items():
print(key, "=", value) # 遍历键值对
# 6. 集合
for x in {1, 2, 3}:
print(x)
4.1 循环控制:break / continue / else
| 关键字 | 作用 | 说明 |
|---|---|---|
break | 跳出整个循环 | 立刻结束,不再执行剩余轮次 |
continue | 跳过本轮剩余部分 | 直接开始下一轮 |
else | 循环正常结束时执行 | 没有被 break 打断才执行 |
break / continue / else 的完整行为
# break:一旦满足立刻跳出
for i in range(10):
if i == 3:
break
print(i)
# 输出 0 1 2
# continue:跳过本轮
for i in range(5):
if i == 2:
continue # 跳过 i==2 这一轮
print(i)
# 输出 0 1 3 4
# else:循环没有被 break 打断时执行(很实用!)
target = 7
for i in range(1, 6):
if i == target:
print(f"找到了 {i}")
break
else:
print(f"{target} 不在 1-5 范围内") # 找不到才执行
# else 也可以用于 while
count = 0
while count < 3:
count += 1
else:
print("循环正常走完", count) # 循环正常走完 3
else 是 Python 的特色:它在循环没有被 break 中断时执行。用它可以优雅表达「搜索完了都没找到」的场景,避免额外维护一个 found 标志变量。continue 在 while 循环里如果忘记更新计数器,会造成死循环(程序永远转圈)。for 循环不受影响,因为它自己会推进迭代器。4.2 for-else 与嵌套 break
两重循环中的 break 只跳出内层
# break 只影响最内层的循环
for i in range(3):
for j in range(3):
if j == 2:
break # 只跳出内层 j 循环
print(i, j)
# 输出:
# 0 0 / 0 1 / 1 0 / 1 1 / 2 0 / 2 1
# 想跳出两层怎么办?用标志变量或把内层封装成函数
def has_pair(target):
for i in range(3):
for j in range(3):
if i + j == target:
return True # 用 return 一次性跳出所有层
return False
print(has_pair(3)) # True
return。代码更短,也不会有忘记清标志位的 bug。4.3 循环遍历时的「边遍历边改」问题
遍历时修改列表会跳过元素
# 危险:边遍历边删除
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 2, 3, 4] —— 4 没被删掉!
# 原因:删除后列表左移,迭代器指针没动
# 正确方案一:遍历副本
nums = [1, 2, 3, 4, 5]
for n in nums[:]:
if n % 2 == 0:
nums.remove(n)
print(nums) # [1, 3, 5]
# 正确方案二:列表推导式重新赋值(第 11 章详讲)
nums = [1, 2, 3, 4, 5]
nums = [n for n in nums if n % 2 != 0]
print(nums) # [1, 3, 5]
# 正确方案三:生成新列表(更高效)
nums = [1, 2, 3, 4, 5]
old, nums = nums, [n for n in nums if n % 2]
while 循环:条件驱动
for 适合「次数/元素已知」,while 适合「不知道次数,只知道继续的条件」。
while 的标准写法
# 基本形式
count = 0
while count < 5:
print(count)
count += 1 # 必须更新,否则死循环
# while-else:条件为假(正常结束)时执行 else
n = 3
while n > 0:
print(n)
n -= 1
else:
print("计数结束")
# 经典场景 1:用户交互直到输入正确
while True:
answer = input("请输入「yes」:").strip().lower()
if answer == "yes":
print("谢谢!")
break
print("输入不对,再试")
# 经典场景 2:处理数据直到结束标记
lines = ["a", "b", "END", "c"]
i = 0
while i < len(lines) and lines[i] != "END":
print("处理:", lines[i])
i += 1
# 经典场景 3:倒计时(模拟)
import time
secs = 3
while secs > 0:
print(f"{secs} ...")
secs -= 1
print("开始!")
5.1 for 还是 while?
| 场景 | 选择 | 原因 |
|---|---|---|
| 遍历列表/字符串/字典 | for | 直接表达「逐个处理」 |
| 已知次数(重试 N 次) | for range(n) | 不需要手动维护计数器 |
| 不确定次数(读到文件尾、用户持续输入) | while | 条件驱动更自然 |
| 遍历时还要用下标 | enumerate | 避免手写下标变量 |
| 需要索引访问或反向 | for + range | Python 负下标很方便 |
同一件事的两种写法
data = [10, 20, 30, 40]
# 写法 A:for + range —— 需要下标时
for i in range(len(data)):
print(i, data[i])
# 写法 B:enumerate —— 更 Pythonic(推荐)
for i, value in enumerate(data):
print(i, value)
# 写法 C:while + 手动下标 —— 能跑但啰嗦,不推荐
i = 0
while i < len(data):
print(i, data[i])
i += 1
for 就用 for。绝大多数「用 while 实现的 for」都可以改写成 for,代码更短也更不容易出 bug。while 真正不可替代的场景是「条件与序列无关」。循环嵌套
循环里可以放循环,形成矩阵式的遍历结构。
嵌套循环的基本形态
# 打印 3x3 表格
for i in range(1, 4): # 外层:行
for j in range(1, 4): # 内层:列
print(f"{i}x{j}", end="\t")
print() # 每行结束换行(end="" 抑制了自动换行)
# 输出:
# 1x1 1x2 1x3
# 2x1 2x2 2x3
# 3x1 3x2 3x3
# 计算 1-100 的乘法表(简版)
for i in range(1, 4):
row = []
for j in range(1, 4):
row.append(f"{i}*{j}={i*j}")
print(" ".join(row))
6.1 嵌套循环的性能与 break 优化
找到就跳出,别白跑
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
target = 5
# 基础写法:找到后用标志位停止
found = False
for row in matrix:
for val in row:
if val == target:
print(f"找到 {target},位置 ({row.index(val)}, ...)")
found = True
break
if found:
break
# 推荐写法:用函数 + return 一次跳出所有层
def locate(data, target):
for i, row in enumerate(data):
for j, val in enumerate(row):
if val == target:
return i, j
return None
pos = locate(matrix, target)
print(pos) # (1, 1)
bisect 模块),复杂度 O(log n)。6.2 嵌套循环的常见应用
三个真实嵌套场景
# 1. 统计词频(二维结构遍历)
text = "the quick brown fox jumps over the lazy dog the end"
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
for word, n in sorted(counts.items(), key=lambda kv: -kv[1])[:3]:
print(f"{word}: {n}")
# 2. 判断素数(试除法)
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
print([n for n in range(2, 20) if is_prime(n)])
# [2, 3, 5, 7, 11, 13, 17, 19]
# 3. 矩阵转置
matrix = [[1, 2, 3], [4, 5, 6]]
rows, cols = len(matrix), len(matrix[0])
transposed = [[matrix[i][j] for i in range(rows)] for j in range(cols)]
print(transposed) # [[1, 4], [2, 5], [3, 6]]
推导式:把循环写进表达式
推导式(comprehension)是 Python 的招牌语法——用一行表达式生成序列,替代 3–4 行循环。正式讲解在第 11 章,这里先掌握最常用的列表推导式。
列表推导式:等价写法对照
# 普通循环
squares = []
for i in range(1, 6):
squares.append(i ** 2)
print(squares) # [1, 4, 9, 16, 25]
# 列表推导式:更短更清晰
squares = [i ** 2 for i in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# 带条件过滤
evens = [i for i in range(10) if i % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
# 带表达式转换
names = ["alice", "bob", "charlie"]
upper = [n.upper() for n in names]
print(upper) # ['ALICE', 'BOB', 'CHARLIE']
# 带三元判断
sizes = [("apple", 0.2), ("melon", 3.0)]
labels = [f"{n}:{'大' if w > 1 else '小'}" for n, w in sizes]
print(labels) # ['apple:小', 'melon:大']
# 嵌套推导式:等价于双层循环
pairs = [(i, j) for i in range(2) for j in range(2)]
print(pairs) # [(0,0), (0,1), (1,0), (1,1)]
# 字典推导式 {键表达式: 值表达式}
squares_dict = {i: i ** 2 for i in range(1, 5)}
print(squares_dict) # {1: 1, 2: 4, 3: 9, 4: 16}
# 集合推导式 {表达式}
letters = {c for c in "hello world" if c in "aeiou"}
print(letters) # 集合,如 {'o', 'e', 'a'}
for i ... for j ... 的顺序等价于外层 i、内层 j。如果加条件,写在对应 for 后面:[i*j for i in R for j in R if i != j]。推导式最多写两层,超过就该用普通循环了。实战:用流程控制解决真实问题
下面三个小项目只用本章所学(分支 + 循环),代码可直接运行。
项目一:简易 ATM 取款模拟
"""模拟 ATM:校验密码、限制额度、统计当日取款次数。"""
MAX_PER_TIME = 5000 # 单次限额
MAX_PER_DAY = 20000 # 每日限额
DAILY_TIMES = 3 # 每日次数上限
balance = 52600.0
daily_used = 0.0
times_used = 0
pin = "1234"
def show_menu():
print("\n====== ATM ======")
print("1. 余额查询")
print("2. 取款")
print("3. 存款")
print("0. 退出")
while True:
show_menu()
choice = input("请选择:").strip()
if choice == "0":
print("感谢使用,再见!")
break
elif choice == "1":
print(f"当前余额:{balance:.2f} 元")
elif choice == "2":
try:
amount = float(input("请输入取款金额:"))
except ValueError:
print("金额必须是数字")
continue
if amount <= 0:
print("取款金额必须大于 0")
elif amount > MAX_PER_TIME:
print(f"单次最多取 {MAX_PER_TIME} 元")
elif times_used >= DAILY_TIMES:
print("今日取款次数已用完")
elif daily_used + amount > MAX_PER_DAY:
remain = MAX_PER_DAY - daily_used
print(f"超出每日限额,今日最多还能取 {remain:.2f} 元")
elif amount > balance:
print("余额不足")
else:
balance -= amount
daily_used += amount
times_used += 1
print(f"取款成功,余额 {balance:.2f} 元"
f"(今日第 {times_used}/{DAILY_TIMES} 次)")
elif choice == "3":
try:
amount = float(input("请输入存款金额:"))
except ValueError:
print("金额必须是数字")
continue
if amount > 0:
balance += amount
print(f"存款成功,余额 {balance:.2f} 元")
else:
print("存款金额必须大于 0")
else:
print("无效选项,请重新输入")
项目二:文本统计与单词云数据准备
"""统计一段文本的字符数、词频、行数,并找出最常见和最罕见的词。"""
from collections import Counter
text = """Python is a programming language.
Python is widely used.
The language Python is easy to learn."""
# 逐行处理
lines = text.splitlines()
print(f"行数:{len(lines)}")
print(f"字符数:{len(text)}(不含换行)")
# 清洗:转小写、只保留字母空格
cleaned = "".join(c.lower() if c.isalpha() or c.isspace() else " " for c in text)
words = cleaned.split()
# 过滤:忽略常见停用词
STOPWORDS = {"is", "a", "the", "to"}
meaningful = [w for w in words if w not in STOPWORDS]
counter = Counter(meaningful)
print(f"有效词数:{len(meaningful)}")
print("\n词频统计:")
for word, n in counter.most_common():
bar = "#" * n
print(f" {word:<12}{n} {bar}")
most = counter.most_common(1)[0]
least = counter.most_common()[-1]
print(f"\n最高频:{most[0]}({most[1]} 次)")
print(f"最低频:{least[0]}({least[1]} 次)")
# 找所有长度大于 4 的唯一单词,按字母排序
long_words = sorted({w for w in meaningful if len(w) > 4})
print("长单词:", long_words)
项目三:九九乘法表与图形绘制
"""用嵌套循环打印九九乘法表和三角形图案。"""
print("===== 九九乘法表 =====")
for i in range(1, 10):
for j in range(1, i + 1):
print(f"{j}×{i}={i * j:<2}", end=" ")
print() # 每行换行
print("\n===== 直角三角形 =====")
for i in range(1, 6):
print("*" * i)
print("\n===== 倒三角 =====")
for i in range(5, 0, -1):
print("*" * i)
print("\n===== 等腰三角形 =====")
n = 5
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))
print("\n===== 菱形 =====")
n = 5
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))
for i in range(n - 1, 0, -1):
print(" " * (n - i) + "*" * (2 * i - 1))
print(..., end=" ") 抑制了行末换行,让内层循环的多个结果挤在同一行;外层循环末尾再补一个空的 print() 换行——这是打印「表格类」输出的标准套路。流程控制的常见坑
| 坑 | 症状 | 解决 |
|---|---|---|
| while 忘记更新计数器 | 程序卡死不退出 | 确认循环体内有 count += 1 |
| continue 在 while 中忘记更新 | 死循环 | 把更新移到循环体最前面 |
| 遍历时修改容器 | 结果不对且不报错 | 遍历副本或生成新列表 |
| range 的右边界 | 少遍历一个元素 | 牢记左闭右开 |
| break 嵌在 for-else 里 | else 不执行 | 确认 break 属于同一个循环 |
| 嵌套循环性能 | 大数据量卡顿 | 改用推导式、itertools 或算法优化 |
| 缩进混用 Tab/空格 | TabError | 统一 4 空格,开自动转换 |
input() 结果当数字用 | TypeError 或比较错误 | 外面包一层 int()/float() |
把「安全输入」写成可复用函数
def ask_number(prompt: str, cast=int) -> float | None:
"""反复提示用户输入合法的数字,返回转换结果或 None。"""
while True:
raw = input(prompt).strip()
if not raw:
continue
try:
return cast(raw)
except ValueError:
print(" 请输入数字,例如 42")
def ask_choice(prompt: str, options: list) -> str:
"""限制输入必须在给定选项内。"""
while True:
pick = input(prompt).strip()
if pick in options:
return pick
print(f" 请输入其中之一:{'/'.join(options)}")
# 使用
age = ask_number("请输入年龄:")
if age is not None:
print(f"明年你就 {int(age) + 1} 岁了")
level = ask_choice("难度?", ["easy", "normal", "hard"])
print("你选了", level)
CPython 字节码视角:编译器怎么把分支与循环翻成指令
前面讲的是「怎么写」,这一节讲「为什么这么写更快」。Python 源码不能直接执行,CPython 的编译器(compiler)会先把源码编译成字节码(bytecode),打包进一个 code 对象,再交给解释器(interpreter)逐条执行。读懂字节码,就能解释两个日常困惑:为什么 for 循环比手写 while 快、为什么 and/or 存在「短路」行为。
把一个函数翻成字节码
import dis # dis = disassembler,反汇编模块
def check(x):
if x > 0:
return "正数"
return "非正数"
dis.dis(check)
输出(为便于阅读,已删掉行号与偏移量):
RESUME 0
LOAD_FAST 0 (x)
LOAD_CONST 0 (0)
COMPARE_OP 2 (>)
POP_JUMP_IF_FALSE 2 to L1 # 条件跳转:假 -> 跳到 L1
LOAD_CONST 1 ('正数')
RETURN_VALUE
L1:
LOAD_CONST 2 ('非正数')
RETURN_VALUE
关键指令是 POP_JUMP_IF_FALSE:它只在条件为假时才跳转,条件为真时直接「直落」(fall through)执行 if 体。这一条决定了整个 Python 分支的骨架:绝大多数代码是直落,跳转只出现在例外路径上。所以把最常见的情况写在第一个 if(或最早的 elif)里,比塞进 else 更快——else 前的每一层 elif 都是一次多余的比较。
for 循环 vs while 循环:汇编层面的差距
import dis, timeit
def loop_for(n: int) -> int:
total = 0
for _ in range(n): # 交给 range 对象的迭代协议驱动
total += 1
return total
def loop_while(n: int) -> int:
total = 0
i = 0
while i < n: # 每轮重新比较 + 手动自增
total += 1
i += 1
return total
print(dis.Bytecode(loop_for).dis())
# GET_ITER # for 开头先取一次迭代器(只执行一次)
# FOR_ITER # 核心指令:向迭代器要下一个值,没有就跳出
# ...
# END_FOR
# FOR_ITER 一步完成「取值 + 判断是否结束 + 跳转」;
# while 版本每轮要额外执行:LOAD_FAST / COMPARE_OP / POP_JUMP_IF_FALSE + 自增
print("for :", timeit.timeit("loop_for(1_000_000)", globals=globals()))
print("while:", timeit.timeit("loop_while(1_000_000)", globals=globals()))
| 维度 | for 版本 | while 版本 |
|---|---|---|
| 核心指令 | GET_ITER(一次)+ FOR_ITER(每轮) | LOAD_FAST + COMPARE_OP + POP_JUMP_IF_FALSE |
| 每轮边界检查 | 在 C 层的迭代器里做 | 在 Python 层每轮重做一遍比较 |
| 自增开销 | 无 | 每轮一次 i += 1(加载、计算、存储三条指令) |
| 100 万次实测 | 约 45 ms | 约 75 ms(约为前者的 1.6 倍) |
| 深层原因 | 迭代推进被特化进解释器主循环 | 比较与自增是彼此独立的通用指令 |
for + range 正是被优化器重点照顾的路径之一。短路求值:字节码里的「提前 RETURN」
import dis
def shortcut(user, admin: bool) -> bool:
return admin or user.is_superuser() # admin 为真时,右半段根本不执行
print(dis.Bytecode(shortcut).dis())
# LOAD_FAST (admin)
# POP_JUMP_FORWARD_IF_TRUE to L1 # 提前跳走,跳过调用
# LOAD_FAST (user)
# LOAD_METHOD (is_superuser)
# CALL
# RETURN_VALUE
# L1:
# LOAD_CONST (True)
# RETURN_VALUE
POP_JUMP_FORWARD_IF_TRUE 在 admin 为真时直接跳过右半段——这就是短路求值(short-circuit evaluation)的字节码证据。它让 x is not None and x.value > 0 这类「保护性写法」既安全又高效:x 为 None 时右半段一次都不会执行。
and/or 返回的不是布尔值,而是操作数本身。a or b 在 a 为真时返回 a,而不是 True。写 result = a or b 前务必确认类型,尤其当 a 可能是 0、"" 时。用 dis 验证 for-else 到底怎么工作
import dis
def search(target: int) -> str:
for i in range(1, 5):
if i == target:
return f"找到 {i}"
else: # 没有 break 才会走这里
return f"{target} 不在 1-4 范围内"
print(dis.Bytecode(search).dis())
# 注意 FOR_ITER 那一行带着一个「跳出循环」的跳转目标,
# 而该目标之后紧跟的正是 else 块的代码 ——
# 这解释了为什么「循环体自然结束」与「跳进 else」在字节码层面是同一条路径。
dis 当成日常调试工具:怀疑某段代码「为什么慢」「为什么没生效」时,先 dis.dis(函数名) 看一眼生成的指令,比盲猜高效得多。需要取文本时用 print(dis.Bytecode(f).dis()),需要逐条程序化分析时用 dis.get_instructions(f)。手写迭代器:迭代器协议、生成器与惰性管道
本章说 for 是「遍历可迭代对象」,但没解释为什么能遍历。答案在 Python 数据模型(data model)的迭代器协议(iterator protocol)里:for 背后其实是两组函数调用——iter() 与 next()。这一节把整套协议手写出来,并延伸到生成器(generator)与生成器表达式(generator expression)。
最小可迭代对象:实现 __iter__ 与 __next__
"""自定义一个「只读计数器」——本质上就是 range 的简化版。"""
class Counter:
def __init__(self, start: int, stop: int, step: int = 1):
self.start, self.stop, self.step = start, stop, step
def __iter__(self): # 返回「迭代器」
"""这里直接返回自己,因为本类同时也是迭代器。"""
return self
def __next__(self): # 返回下一个值
if self.step > 0 and self.start >= self.stop:
raise StopIteration # 必须抛这个异常来收尾
if self.step < 0 and self.start <= self.stop:
raise StopIteration
value = self.start
self.start += self.step
return value
def __len__(self): # 可选:让 len() 也能用
span = abs(self.stop - self.start)
step = abs(self.step)
return 0 if span == 0 else (span - 1) // step + 1
for i in Counter(1, 5):
print(i, end=" ") # 1 2 3 4
print()
print(len(Counter(1, 5))) # 4
| 魔术方法 | 英文原名 | 被谁调用 | 作用 |
|---|---|---|---|
__iter__ | iterator | iter(obj)、for | 交出一个迭代器 |
__next__ | next | next(it)、for 主循环 | 逐个产出元素 |
__len__ | length | len(obj) | 报告元素个数 |
__contains__ | contains | x in obj | 自定义成员判断(可绕开线性扫描) |
__getitem__ | get item | obj[i] | 按下标取值,也提供旧式迭代兜底 |
for x in obj: 等价于下面这四行。找不到 __iter__ 时,iter() 会退而寻找 __getitem__,并按 0、1、2…… 逐个取值直到抛 IndexError(旧式序列协议)。_it = iter(obj)
while True:
try:
x = next(_it) # StopIteration 时跳出
except StopIteration:
break
...循环体...
生成器:用 yield 写迭代器
"""生成器函数(generator function)被调用时返回的是生成器对象。"""
def fib():
"""无限斐波那契数列——只有下游真的要下一个值时才继续算。"""
a, b = 0, 1
while True:
yield a # 产出当前值,并把执行「冻结」在这一行
a, b = b, a + b # 下游再次 next 时从这里恢复
gen = fib() # 此刻一行都还没执行
print(next(gen)) # 0
print(next(gen)) # 1
print([next(gen) for _ in range(5)]) # [1, 2, 3, 5, 8]
from itertools import islice # 把无限序列切成有限的一段
print(list(islice(fib(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
生成器表达式:把方括号换成圆括号
# 列表推导式 -> 立刻算出整个结果
squares_list = [i * i for i in range(1_000_000)] # 实打实占几十 MB
# 生成器表达式(圆括号)-> 边算边给,用多少算多少
squares_gen = (i * i for i in range(1_000_000)) # 几乎不占内存
from itertools import islice
print(list(islice(squares_gen, 5))) # [1, 4, 9, 16, 25]
# 内置函数能直接吃生成器,不必先转 list
print(sum(i for i in range(1_000_000))) # 499999500000,额外内存 O(1)
print(max(i * i for i in range(100_000)))
print(any(i % 7 == 0 for i in range(100))) # True,短路,找到就停
print(all(i % 2 == 0 for i in range(100_000))) # False,遇到奇数就停
# 但生成器不支持切片与索引 —— 一次性的
try:
squares_gen[0]
except TypeError as e:
print(e) # 'generator' object is not subscriptable
实战:手写一个惰性分页读取器
"""把一个大列表按页惰性产出:处理完这页才会去算下一页。"""
from collections.abc import Iterator
from typing import TypeVar
T = TypeVar("T")
def paginate(items: list[T], size: int) -> Iterator[list[T]]:
if size <= 0:
raise ValueError("size 必须为正整数")
for start in range(0, len(items), size): # range 本身是惰性的
yield items[start:start + size]
records = [f"r{i}" for i in range(10)]
for page_no, page in enumerate(paginate(records, 3), start=1):
print(f"第 {page_no} 页:{page}")
# 第 1 页:['r0', 'r1', 'r2']
# 第 2 页:['r3', 'r4', 'r5']
# 第 3 页:['r6', 'r7', 'r8']
# 第 4 页:['r9']
for 会静默地什么都不产出——不报错,只是空结果,极难排查。需要重复消费就调两次函数,或 list(gen) 存下来。另一个坑:生成器函数在第一次 next() 之前不会执行任何一行,包括函数体开头的参数校验。循环性能实测:把百万次循环压到几十毫秒
循环写法不只是风格问题。本节用 timeit 做可复现的基准测试(benchmark),逐级优化同一个任务——统计列表中满足条件的元素个数——看清每一步优化到底带来多少收益,以及哪些「提速技巧」其实是负优化。
先写一个口径统一的计时器
import timeit
def bench(func, arg, number=3):
"""统一口径:先预热一次,排除首次调用的冷启动干扰,再计时。"""
func(arg) # 预热
t = timeit.timeit(lambda: func(arg), number=number)
print(f"{func.__name__:<22}{t * 1e3:8.2f} ms") # 统一换算成毫秒
return t
五个版本,从最慢优化到最快
data = list(range(1_000_000)) # 100 万个整数
THRESHOLD = 500_000
# v1 手写 while + 下标 —— 最啰嗦
def v1_while_index(data, k=THRESHOLD):
i = 0
n = 0
while i < len(data): # 每轮都调用一次 len()
if data[i] > k:
n += 1
i += 1
return n
# v2 for + range(len(data)) —— 好一点,但每次还要按下标取值
def v2_for_range(data, k=THRESHOLD):
n = 0
for i in range(len(data)):
if data[i] > k:
n += 1
return n
# v3 for 直接遍历值 —— 推荐起点
def v3_for_value(data, k=THRESHOLD):
n = 0
for v in data:
if v > k:
n += 1
return n
# v4 sum(生成器表达式) —— 累加下沉到 C 层
def v4_sum_genexp(data, k=THRESHOLD):
return sum(1 for v in data if v > k)
# v4b sum(布尔) —— 也能跑,但每个 True 都多创建一个 bool 对象
def v4b_sum_bool(data, k=THRESHOLD):
return sum(v > k for v in data)
for f in (v1_while_index, v2_for_range, v3_for_value, v4_sum_genexp, v4b_sum_bool):
bench(f, data)
# 典型结果(x86-64 / CPython 3.12,随机器浮动):
# v1_while_index 182.44 ms
# v2_for_range 104.91 ms
# v3_for_value 71.35 ms
# v4_sum_genexp 46.20 ms
# v4b_sum_bool 63.08 ms
| 版本 | 实测耗时 | 相对 v1 | 瓶颈在哪 |
|---|---|---|---|
| v1 while + 下标 | 约 182 ms | 1.0×(基准) | 每轮一次 len() 调用 + 手动自增 |
| v2 for + range(len) | 约 105 ms | 1.7× | 多了一次 data[i] 下标查找 |
| v3 for 直接遍历值 | 约 71 ms | 2.6× | 逐元素的 Python 层比较 |
| v4 sum(生成器) | 约 46 ms | 3.9× | sum 的累加在 C 层完成 |
| v4b sum(布尔) | 约 63 ms | 2.9× | 每个元素多一次 bool 对象创建 |
sum/any/all/min/max/in),而不是把一种 for 改写成另一种 for。负优化:这些「提速技巧」其实更慢
data = list(range(1_000_000))
K = 500_000
def bad_manual_append(data, k=K):
out = []
for v in data:
if v > k:
out.append(1) # 每轮都做属性查找 + 方法调用
return len(out)
def good_listcomp(data, k=K):
return len([1 for v in data if v > k]) # 推导式用专用 LIST_APPEND 指令
def bad_string_join(data, k=K):
# 先把每个元素变成字符串再拼起来,纯属浪费两个数量级
return len("".join(str(v) for v in data if v > k))
def good_sum(data, k=K):
return sum(1 for v in data if v > k) # 既不建列表也不建字符串
# 100 万元素实测:
# good_sum 约 46 ms
# good_listcomp 约 55 ms (结果一致,只差在多分配了一个列表)
# bad_manual_append 约 71 ms (比推导式慢约 30%)
# bad_string_join 约 210 ms (字符串化是数量级的代价)
len()、或把 for 改写成 while——在 3.12 这些几乎普遍是负优化。真正要警惕的是复杂度:嵌套双层循环 O(n²) 再怎么微优化也救不了,必须换算法。数量级优化:把 O(n×m) 的嵌套循环换成 O(n+m)
import random, time
random.seed(42)
small = random.sample(range(10 ** 9), 2000) # 2000 个互不相同的大数
big = random.sample(range(10 ** 9), 2000)
# 慢:嵌套循环 2000 × 2000 = 400 万次比较
t0 = time.perf_counter()
pairs_slow = [(a, b) for a in small for b in big if a == b]
t_slow = time.perf_counter() - t0
# 同样两层循环,但用 set 把成员判断从 O(m) 降到 O(1)
t0 = time.perf_counter()
lookup = set(big)
pairs_fast = [(a, b) for a in small for b in big if b in lookup and a == b]
t_fast = time.perf_counter() - t0
print(f"嵌套循环:{t_slow * 1000:.2f} ms")
print(f"集合查找:{t_fast * 1000:.2f} ms")
# 典型:嵌套循环约 210 ms / 集合查找约 0.6 ms —— 三百倍以上的差距
# 最常见的真实场景:去重保序
seen: set[int] = set()
dups: list[int] = [] # 重复项用列表保留出现顺序
for x in [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]:
if x in seen: # O(1),而不是列表的 O(n)
dups.append(x)
seen.add(x)
print("重复项:", dups) # [1, 5, 3](按首次重复的顺序)
python -m cProfile -s cumtime 你的脚本.py 找真正的热点,再用 line_profiler 逐行计时。凭直觉优化最大的风险是「把时间花在只占 2% 的代码上」;而一旦发现热点是嵌套循环,第一反应应该是「能不能换成 set/dict 查表或 itertools」,而不是去抠循环体里的写法。工程边界:类型收窄、穷尽性检查与百万级循环的写法选型
真实项目里的分支循环不是孤立的:它们要和类型系统、错误处理、以及百万级以上的数据打交道。这一节讲三件工程上必须做对的事——用类型标注(type annotation)让分支收窄(narrowing)可被静态检查、用穷尽性检查(exhaustiveness checking)保证新增分支不遗漏、面对海量循环时选对写法。
类型标注 + 分支收窄
from enum import StrEnum # 3.11+ 新增,字符串枚举
class Level(StrEnum):
DEBUG = "debug"
INFO = "info"
WARN = "warn"
ERROR = "error"
def render(level: Level) -> str:
# 参数标注成 Level 之后,写 level == "debug" 会被静态检查器直接标红
if level is Level.DEBUG:
return "…详细堆栈…"
elif level is Level.WARN:
return "…带警告标记的短消息…"
return level.value
def parse(text: str) -> Level:
"""把用户输入安全地映射成枚举。"""
match text.strip().lower(): # 3.10+ 模式匹配做映射
case "d" | "debug":
return Level.DEBUG
case "i" | "info":
return Level.INFO
case "w" | "warn":
return Level.WARN
case "e" | "error":
return Level.ERROR
case _:
raise ValueError(f"无法识别的级别:{text!r}")
# 显式类型收窄:isinstance 分支让类型检查器把 x 的类型缩窄
def size_of(value: int | str | None) -> int:
if isinstance(value, int):
return value # 此分支内 value 就是 int
if isinstance(value, str):
return len(value)
raise TypeError("不接受 None")
| 场景 | 不推荐写法 | 推荐写法 | 原因 |
|---|---|---|---|
| 状态取值 | "debug" 裸字符串 | StrEnum 成员 | 裸字符串拼错要到运行时才炸,枚举拼错在静态检查就炸 |
| 多分支映射 | 一长串 if/elif | match + | 合并 | 结构对称,新增别名只改一处 |
| 临时标志位 | found = False | for ... else | 少一个需要手工维护的状态变量 |
| 类型收窄 | 无标注的 isinstance 分支 | 标注 Union + isinstance | 类型检查器能自动缩窄 x 的类型 |
| 可迭代但可能为空 | for x in maybe_list | for x in maybe_list or () | 避免对 None 迭代抛 TypeError |
穷尽性检查:让「漏掉的分支」暴露出来
"""显式拒绝所有未处理的形态——漏写分支时不会静默走错。"""
def summarize(cmd: tuple[str, ...]) -> str:
match cmd:
case ("move", direction):
return f"移动到 {direction}"
case ("quit",):
return "退出"
case _ as other:
# 最后一道防线:任何未支持的命令都会在这里被显式拒绝
raise NotImplementedError(f"尚未支持的命令:{other!r}")
# 更进一步:用 Literal 声明全部可能返回值,交给 mypy / pyright 检查
from typing import Literal
Outcome = Literal["moved", "quit", "unknown"]
def outcome_of(cmd: str) -> Outcome: # 声明只有这三种结果
match cmd:
case "move":
return "moved"
case "quit":
return "quit"
case _:
return "unknown"
# 用法:模式匹配 + 类型标注,IDE 会在你新增分支时立刻给出提示
def describe(value: int | str | None) -> str:
match value:
case None:
return "空值"
case int() if value > 0:
return f"正数 {value}"
case str() as s if s:
return f"非空字符串:{s}"
case _:
return f"其他:{value!r}"
match 里的 case int()、case str() 是类模式(class pattern),会走 isinstance,并且先匹配先赢:把宽泛的 case object: 或 case _: 写在前面,后面的细分支就永远不会执行。务必具体模式在前、case _ 在最后。另外注意类模式会调用构造函数做模式匹配(case Point(0, 0)),别误当成 == 比较。海量循环:写法选型与内存剖析
import tracemalloc # 标准库内存剖析器
def by_loop(n: int) -> list:
out = []
for i in range(n):
out.append(i * i) # 结果全量驻留
return out
def by_listcomp(n: int) -> list:
return [i * i for i in range(n)] # 同结果,通常更快
def by_genexp(n: int):
return (i * i for i in range(n)) # 生成器:几乎不占内存
tracemalloc.start()
base = tracemalloc.get_traced_memory()[0]
a = by_loop(2_000_000)
peak_loop = tracemalloc.get_traced_memory()[1] - base
del a
b = by_listcomp(2_000_000)
peak_comp = tracemalloc.get_traced_memory()[1] - base
del b
c = by_genexp(2_000_000) # 只取前 2 个就丢掉
next(c); next(c)
peak_gen = tracemalloc.get_traced_memory()[1] - base
print(f"循环 + append 峰值:{peak_loop / 1e6:8.1f} MB")
print(f"列表推导式峰值: {peak_comp / 1e6:8.1f} MB")
print(f"生成器表达式峰值: {peak_gen / 1e6:8.1f} MB")
# 典型:append ≈ 推导式 >> 生成器(后者接近 0)
| 需求 | 选型 | 内存 | 说明 |
|---|---|---|---|
| 要全部结果、结果不大 | 列表推导式 | O(n) | 最快最简洁的默认选择 |
| 要全部结果、结果很大 | 生成器 + 分批落盘 | O(批大小) | 避免 OOM(内存耗尽) |
| 只要一个汇总数字 | sum/any/all/max | O(1) | 循环下沉到 C 层,最快,且能短路 |
| 数据来自文件或网络 | 逐行迭代 + 生成器管道 | O(一行) | 流式处理,天生惰性 |
| 百万级纯计算 | 先想能否交给 itertools / math.prod | O(1) | itertools 是 C 实现,常比 Python 循环快数倍 |
itertools.groupby、itertools.product、math.prod、functools.reduce 都是现成的「循环下沉」工具。另外别忘了删掉 enumerate 里没用的下标,每轮都在白算一个整数。tracemalloc 量一量,而不是等它跑挂。本章小结
- 缩进即语法:Python 用缩进界定代码块,统一 4 空格,不需要
end。 - 分支三件套:
if/elif(可多个)/else(最多一个,必须最后)。 - 条件求真值:
if names:判断非空、if not user:判断 None,比== []更地道。 - 卫语句:把否定分支用
return提前处理掉,避免三层以上嵌套。 - 三元表达式:
"及格" if score >= 60 else "不及格",是表达式不是语句。 match(3.10+):结构化模式匹配,适合按「形状」分派,配合case _:兜底。range(start, stop, step):左闭右开、惰性、支持反向,range(len(x))是默认计数约定。for遍历可迭代对象:字典默认遍历键,用.items()拿键值对。break/continue/else:break跳全循环,continue跳本轮,else在未被 break 时执行。- break 只跳一层:要跳多层用「函数 + return」。
- 别边遍历边改:用副本遍历或生成新列表,否则会静默跳过元素。
forvswhile:能用for就用for;while用于次数未知、只知条件。- 推导式:
[expr for x in seq if cond]替代「建列表 + 循环 + append」,更快也更短。 - 嵌套循环:复杂度相乘,大数据量要警惕;有序数据查找用
bisect。 - 输入守卫:把校验重试封装成函数,交互程序才写得干净。
练习题
- FizzBuzz:打印 1–100,遇到 3 的倍数打印
Fizz,5 的倍数打印Buzz,同时是两者打印FizzBuzz。要求用for+range实现,并再写一版用while的。 - 数字游戏:猜数与反馈:随机生成 1–100 的整数,让用户猜;每次猜完给出「大了/小了/对了」,并统计次数;限制 7 次机会内猜中,超出则公布答案。要求同时支持
for(次数已知)和while(次数由条件控制)两种写法。 - 猜数游戏的升级:输入校验:在上一题基础上,处理非法输入:非数字、超过 100、小于 1、猜重复的数,都要给出不同提示且不消耗次数。要求把校验写成独立的
loop函数。 - 百钱买百鸡:用三重嵌套循环求解——公鸡 5 元一只、母鸡 3 元一只、小鸡 1 元三只,用 100 元恰好买 100 只鸡,有几种解法?每种解法打印三种鸡的数量。要求用
break提前结束不可能的循环来优化。 - 完数与因子:找出 1–100 的所有「完数」(因子之和等于自身,如 6、28)。并对给定的 n,输出它的所有因子。要求用
for + range,并说明为什么循环上限取到int(n ** 0.5) + 1而不是n。 - 列表推导式重构:把以下代码全部改写成推导式(一行一个),并对比行数:① 生成 1–100 中 3 的倍数;② 把
["1","2","3"]转成整数列表;③ 从列表中筛出长度大于 3 的单词;④ 把列表中的负数转成绝对值;⑤ 生成{"a": 1, "b": 2}的反向字典。 - for-else 的实战用法:用
for+else实现「在字符串中查找子串」的功能:找到就break并打印位置,没找到(else)时打印「未找到」。不许使用in运算符。 - while 转 for 改造:把下面这段
while代码改写成for,说明为什么改写更好:i = 0 data = [5, 3, 8, 1] total = 0 while i < len(data): total += data[i] i += 1 print(total) - 菜单驱动的通讯录:写一个命令行程序,支持「1 添加联系人 / 2 查找联系人 / 3 列出全部 / 4 删除联系人 / 0 退出」,用
dict存数据(姓名为键,电话为值),每个操作都要校验输入合法性。删除不存在的联系人时要有友好提示。 - 交通灯状态机(用 match):用
match语句实现一个交通灯控制器:输入"red"/"yellow"/"green",输出对应的通行规则描述;输入其他内容输出错误提示。再用if-elif重写一遍,比较哪种更易读。 - 综合:九九乘法表变形:分别用
for嵌套和双重列表推导式两种方式打印完整的 9×9 乘法表(要求对齐、每行一个三角形)。比较两种写法的可读性。
for i in range(1, 101):
if i % 15 == 0:
print("FizzBuzz")
elif i % 3 == 0:
print("Fizz")
elif i % 5 == 0:
print("Buzz")
else:
print(i)
i = 1
while i <= 100:
if i % 15 == 0: print("FizzBuzz")
elif i % 3 == 0: print("Fizz")
elif i % 5 == 0: print("Buzz")
else: print(i)
i += 1
for 版用 for _ in range(7): 配合 break;while 版用 attempts < 7 and not won 作条件。循环的 else 分支正好用来处理「用完次数还没猜中」。def ask_guess(seen):
while True:
raw = input("请输入 1-100 的整数:").strip()
try:
v = int(raw)
except ValueError:
print(" 必须是整数"); continue
if not 1 <= v <= 100:
print(" 超出范围"); continue
if v in seen:
print(" 这个数猜过了"); continue
seen.add(v)
return v
z = 100 - x - y。答案有唯一解:公鸡 3 只、母鸡 19 只、小鸡 78 只。优化点:if 5*x + 3*y + z/3 == 100 中 z 必须是 3 的倍数,可加 if z % 3: continue。def divisors(n):
result = []
for i in range(1, int(n ** 0.5) + 1):
if n % i == 0:
result.append(i)
if i != n // i: # 避免完全平方数重复加
result.append(n // i)
return sorted(result)
for n in range(1, 101):
if n > 1 and sum(divisors(n)) - n == n:
print("完数:", n) # 6 28
说明:因子成对出现(i 与 n//i),只需遍历到平方根,复杂度从 O(n) 降到 O(√n)。
# ①
[n for n in range(1, 101) if n % 3 == 0]
# ②
[int(s) for s in ["1", "2", "3"]]
# ③
[w for w in words if len(w) > 3]
# ④
[abs(n) for n in nums]
# ⑤
{v: k for k, v in d.items()}
text, sub = "hello world", "world"
for i in range(len(text) - len(sub) + 1):
if text[i:i + len(sub)] == sub:
print(f"在位置 {i} 找到")
break
else:
print("未找到")
print(sum(data)) # 甚至可以一步到位
# 或:
total = 0
for v in data:
total += v
for 版少了一个需要手动维护的 i,不会因为忘记 i += 1 而死循环,也不需要 len(data)。
while True + break 退出;查找用 in 判断键存在;删除用 del d[name](可先 pop(name, None) 避免 KeyError);列表可用 for name, phone in d.items() 遍历。msg = {
"red": "红灯:所有车辆停止,行人通行",
"yellow": "黄灯:减速通过,即将变红",
"green": "绿灯:正常通行",
}
match light:
case "red" | "green" | "yellow":
print(msg[light])
case _:
print("无效的灯色")
结论:状态少时 if-elif 更直白;状态多且需要按「形状/范围」分派时 match 更有优势。
table = [[f"{j}×{i}={i*j}" for j in range(1, i + 1)] for i in range(1, 10)]
for row in table:
print(" ".join(row))
推导式版本更短,但当每行的「列数」与「行号」相关时,嵌套推导式的可读性其实不如双层 for——这也是「不要为了炫技而用推导式」的例子。