本章

基础篇 / 第 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("不及格")
要点Python 允许空行和注释出现在代码块中任意位置,解释器会忽略它们。这让长代码块更容易阅读。
技巧把编辑器设置成「Tab 自动转 4 空格」并显示空白字符(VS Code 里按 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 "可以下单"
技巧卫语句写法的好处:① 阅读时「排除法」比「包含法」更直观;② 缩短了主路径的缩进层级;③ 减少了「漏写 else 分支」的概率。

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=62 3 4 5
range(0, 10, 2)step=20 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 里索引计数的默认约定。
技巧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]
常见坑「遍历列表时删除元素」是 Python 最高频的逻辑 bug 之一,且不报错、结果还错得莫名其妙。记住口诀:要么遍历副本,要么生成新列表,不要原地改正在遍历的容器。

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 + rangePython 负下标很方便

同一件事的两种写法

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)
常见坑双层循环的复杂度是 O(n×m)。如果两个循环都是 10000 次,就是 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)
要点把「校验 + 重试」封装成一个小函数,能让主流程代码清爽十倍。这类「输入守卫(input guard)」函数是任何交互式程序的必备组件。

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 倍)
深层原因迭代推进被特化进解释器主循环比较与自增是彼此独立的通用指令
要点这不是玄学。CPython 3.11 起引入了特化字节码(specializing adaptive interpreter),3.12 继续强化这套机制:解释器会针对高频指令和常见对象类型生成更快的专用版本。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__iteratoriter(obj)、for交出一个迭代器
__next__nextnext(it)、for 主循环逐个产出元素
__len__lengthlen(obj)报告元素个数
__contains__containsx in obj自定义成员判断(可绕开线性扫描)
__getitem__get itemobj[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
技巧什么时候该用生成器:① 数据能「一个一个算出来」而不是「一次性全在内存里」;② 只消费一次;③ 数据量可能远超内存;④ 不想提前把结果包成列表。典型场景是读大文件、构建数据管道(pipeline)、做惰性分页。

实战:手写一个惰性分页读取器

"""把一个大列表按页惰性产出:处理完这页才会去算下一页。"""
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 ms1.0×(基准)每轮一次 len() 调用 + 手动自增
v2 for + range(len)约 105 ms1.7×多了一次 data[i] 下标查找
v3 for 直接遍历值约 71 ms2.6×逐元素的 Python 层比较
v4 sum(生成器)约 46 ms3.9×sum 的累加在 C 层完成
v4b sum(布尔)约 63 ms2.9×每个元素多一次 bool 对象创建
要点性能收益分三层依次递减:①循环次数 → ②每轮的字节码条数 → ③能否把整段循环下沉到 C 层。真正的加速来自第三层(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/elifmatch + | 合并结构对称,新增别名只改一处
临时标志位found = Falsefor ... else少一个需要手工维护的状态变量
类型收窄无标注的 isinstance 分支标注 Union + isinstance类型检查器能自动缩窄 x 的类型
可迭代但可能为空for x in maybe_listfor 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/maxO(1)循环下沉到 C 层,最快,且能短路
数据来自文件或网络逐行迭代 + 生成器管道O(一行)流式处理,天生惰性
百万级纯计算先想能否交给 itertools / math.prodO(1)itertools 是 C 实现,常比 Python 循环快数倍
技巧想让循环真的跑得动,优先考虑两件事:①把「多次遍历」合并成一次(一次循环里同时统计多个指标,而不是跑三遍);②把 Python 层循环交给 C 层——itertools.groupby、itertools.product、math.prod、functools.reduce 都是现成的「循环下沉」工具。另外别忘了删掉 enumerate 里没用的下标,每轮都在白算一个整数。
要点这一节三个动作指向同一件事:把错误从运行时挪到编译期。类型标注让「写错枚举名」在保存文件时就报错;穷尽性检查让「漏掉一个分支」在提交前检查时就报错;而海量循环里最容易出的 OOM 或「跑了十分钟」,在写代码之前就该先用 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」。
  • 别边遍历边改:用副本遍历或生成新列表,否则会静默跳过元素。
  • for vs while:能用 for 就用 for;while 用于次数未知、只知条件。
  • 推导式:[expr for x in seq if cond] 替代「建列表 + 循环 + append」,更快也更短。
  • 嵌套循环:复杂度相乘,大数据量要警惕;有序数据查找用 bisect。
  • 输入守卫:把校验重试封装成函数,交互程序才写得干净。

练习题

  1. FizzBuzz:打印 1–100,遇到 3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是两者打印 FizzBuzz。要求用 for + range 实现,并再写一版用 while 的。
  2. 参考答案要点
    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
  3. 数字游戏:猜数与反馈:随机生成 1–100 的整数,让用户猜;每次猜完给出「大了/小了/对了」,并统计次数;限制 7 次机会内猜中,超出则公布答案。要求同时支持 for(次数已知)和 while(次数由条件控制)两种写法。
  4. 参考答案要点for 版用 for _ in range(7): 配合 break;while 版用 attempts < 7 and not won 作条件。循环的 else 分支正好用来处理「用完次数还没猜中」。
  5. 猜数游戏的升级:输入校验:在上一题基础上,处理非法输入:非数字、超过 100、小于 1、猜重复的数,都要给出不同提示且不消耗次数。要求把校验写成独立的 loop 函数。
  6. 参考答案要点
    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
  7. 百钱买百鸡:用三重嵌套循环求解——公鸡 5 元一只、母鸡 3 元一只、小鸡 1 元三只,用 100 元恰好买 100 只鸡,有几种解法?每种解法打印三种鸡的数量。要求用 break 提前结束不可能的循环来优化。
  8. 参考答案要点三重循环枚举 x(公鸡)、y(母鸡)、z 由总数和金额推出;或降为两重循环用 z = 100 - x - y。答案有唯一解:公鸡 3 只、母鸡 19 只、小鸡 78 只。优化点:if 5*x + 3*y + z/3 == 100 中 z 必须是 3 的倍数,可加 if z % 3: continue。
  9. 完数与因子:找出 1–100 的所有「完数」(因子之和等于自身,如 6、28)。并对给定的 n,输出它的所有因子。要求用 for + range,并说明为什么循环上限取到 int(n ** 0.5) + 1 而不是 n。
  10. 参考答案要点
    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)。
  11. 列表推导式重构:把以下代码全部改写成推导式(一行一个),并对比行数:① 生成 1–100 中 3 的倍数;② 把 ["1","2","3"] 转成整数列表;③ 从列表中筛出长度大于 3 的单词;④ 把列表中的负数转成绝对值;⑤ 生成 {"a": 1, "b": 2} 的反向字典。
  12. 参考答案要点
    # ①
    [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()}
  13. for-else 的实战用法:用 for + else 实现「在字符串中查找子串」的功能:找到就 break 并打印位置,没找到(else)时打印「未找到」。不许使用 in 运算符。
  14. 参考答案要点
    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("未找到")
  15. while 转 for 改造:把下面这段 while 代码改写成 for,说明为什么改写更好:
    i = 0
    data = [5, 3, 8, 1]
    total = 0
    while i < len(data):
        total += data[i]
        i += 1
    print(total)
  16. 参考答案要点
    print(sum(data))        # 甚至可以一步到位
    # 或:
    total = 0
    for v in data:
        total += v
    for 版少了一个需要手动维护的 i,不会因为忘记 i += 1 而死循环,也不需要 len(data)。
  17. 菜单驱动的通讯录:写一个命令行程序,支持「1 添加联系人 / 2 查找联系人 / 3 列出全部 / 4 删除联系人 / 0 退出」,用 dict 存数据(姓名为键,电话为值),每个操作都要校验输入合法性。删除不存在的联系人时要有友好提示。
  18. 参考答案要点主循环用 while True + break 退出;查找用 in 判断键存在;删除用 del d[name](可先 pop(name, None) 避免 KeyError);列表可用 for name, phone in d.items() 遍历。
  19. 交通灯状态机(用 match):用 match 语句实现一个交通灯控制器:输入 "red"/"yellow"/"green",输出对应的通行规则描述;输入其他内容输出错误提示。再用 if-elif 重写一遍,比较哪种更易读。
  20. 参考答案要点
    msg = {
        "red":    "红灯:所有车辆停止,行人通行",
        "yellow": "黄灯:减速通过,即将变红",
        "green":  "绿灯:正常通行",
    }
    match light:
        case "red" | "green" | "yellow":
            print(msg[light])
        case _:
            print("无效的灯色")
    结论:状态少时 if-elif 更直白;状态多且需要按「形状/范围」分派时 match 更有优势。
  21. 综合:九九乘法表变形:分别用 for 嵌套和双重列表推导式两种方式打印完整的 9×9 乘法表(要求对齐、每行一个三角形)。比较两种写法的可读性。
  22. 参考答案要点
    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——这也是「不要为了炫技而用推导式」的例子。
Python 完全教程 · 第 3 章