第三章 流程控制语句

第三章 流程控制语句

第二章那张便利店小票,写的时候挺得意。三样商品,对得整整齐齐。回头再看才发现不对劲,那三行 print 有点冗余。货架上要是摆着三百样东西,这办法当场就废了,而且还得祈祷每一行都是写正确的。

上面这段冗余代码要靠这章解决。把重复的动作交给循环,人只描述一次规则,代码能短一半,改起来只改一处。

目录

python 复制代码
第三章  流程控制语句
│
├── 1. 程序结构        → 顺序 / 选择 / 循环,以及流程图怎么画
├── 2. 选择语句        → if / if-else / if-elif-else / match-case
├── 3. 条件表达式      → 一行写完的分支,以及它的几个暗坑
├── 4. 循环语句        → while / for / range / 遍历技巧 / else 子句
├── 5. 跳转语句        → break / continue,以及如何跳出多层循环
├── 6. pass 空语句     → 什么都不做,但必须有
├── 7. 实战            → 成绩评级 / 猜数字 / 九九表 / 素数 / 菜单系统
└── 8. 小结            → 一张表收口 + 避坑清单 + 5 道题 + 下一章预告

照旧,别只阅读。建个第三章目录,代码敲进去跑一遍,改改参数再跑一遍。这一章的东西光看是看不会的,要敲。


一、程序结构

1.1 三种基本结构

上世纪六十年代就有人证明了,任何复杂的程序逻辑都可以用三种结构拼出来。翻译成人话是,你将来写的每一行业务逻辑,往上翻底牌,都是顺序、选择、循环这三样。

顺序结构最朴素。代码从上往下走,一行接一行,不拐弯。

css 复制代码
    ┌──────────┐
    │  语句 A  │
    └────┬─────┘
         │
    ┌────▼─────┐
    │  语句 B  │
    └────┬─────┘
         │
    ┌────▼─────┐
    │  语句 C  │
    └──────────┘

选择结构开始有岔路。碰到一个判断,符合条件走一边,不符合走另一边。

markdown 复制代码
              ┌────────────┐
              │  条件成立?  │
              └──────┬─────┘
            真 ┌─────┴─────┐ 假
               │           │
    ┌──────────▼──┐   ┌────▼─────────┐
    │   代码块 1   │   │   代码块 2    │
    └──────┬──────┘   └──────┬───────┘
           │                 │
           └────────┬────────┘
                    │
              ┌─────▼─────┐
              │  继续往下  │
              └───────────┘

循环结构多了一条回头路。一组语句干完,条件还满足就再干一遍。

markdown 复制代码
         ┌──────────┐
         │ 初始化变量 │
         └────┬─────┘
              │
       ┌──────▼──────┐
       │   条件成立?  │◄───────┐
       └──────┬──────┘        │
              │ 真            │
              │               │
       ┌──────▼──────┐        │
       │   循环体     │────────┘
       └──────┬──────┘
              │ 假
              │
       ┌──────▼──────┐
       │  继续往下    │
       └─────────────┘

这三种结构有个共同点,都只有一个入口和一个出口。听着像废话,好处却很实在。块内代码可以单独拿出来读,读完就知道它干了什么,不用追着跳转跑遍整个文件。

1.2 流程图怎么画

上面这些叫流程图,是仅次于代码的第二种表达方式。写复杂逻辑之前画一张,能省掉一半调试时间。这套符号是通用的。

形状 名字 什么时候用
圆角矩形 起止框 开始、结束
矩形 处理框 一条普通操作,赋值、计算
菱形 判断框 二选一的条件分流
平行四边形 输入输出框 读数据、打印结果
带箭头的线 流程线 标执行方向

拿「输入一个成绩,及格打印通过,否则打印补考」练一下。

arduino 复制代码
   ╭─────────────╮
   │     开始     │
   └──────┬──────┘
          │
   ╱──────▼───────╲
   │  输入 score   │      ← 输入
   ╲──────┬───────╱
          │
      ┌───┴────┐
      │score>=60│
      └───┬────┘
     真 ┌──┴──┐ 假
        │     │
 ┌──────▼──┐ ┌▼──────────┐
 │ 打印"通过"│ │ 打印"请补考" │
 └──────┬──┘ └┬──────────┘
        │     │
        └──┬──┘
           │
    ╭──────▼──────╮
    │     结束     │
    ╰─────────────╯

这张图翻译成 Python 只有五行。

python 复制代码
score = int(input("请输入成绩 >> "))

if score >= 60:
    print("通过")
else:
    print("请补考")

图和代码的对应关系比看上去紧密。菱形对应 if,菱形的两条出边对应 if 和 else 两个块,两条边汇合的地方就是两个块外面那行代码。

1.3 代码块与缩进

第一章提过缩进,这里把它和结构对上号。Python 里 if、elif、else、for、while、def、class 后面跟着的同一个缩进区间,就是一块代码。

bash 复制代码
if 今天下雨:             ← 冒号宣布下面要开新块
    ┌──────────────────┐
    │ 带伞              │
    │ 打车              │   缩进一样 = 同一块 = 一起生一起死
    └──────────────────┘
elif 今天有霾:           ← 又一块
    ┌──────────────────┐
    │ 戴口罩            │
    └──────────────────┘
else:                    ← 还是一块
    ┌──────────────────┐
    │ 骑车              │
    └──────────────────┘
出门                     ← 退回行首,出块了,无论走哪条分支都会到这

缩进级别相同就是同一层。这个约定看着松散,其实比花括号严格得多。花括号语言里,大括号里面一行都不缩进照样能编译,Python 不给你这个自由。

有个容易忽略的细节,空行不影响代码块。块中间插一行空行,Python 照样当它是块的一部分。Tab 和空格千万别混着来,报错信息会让你怀疑人生。

1.4 三种结构组合起来

真实的程序从来不是单一结构。看一个早上出门的例子,三种结构全在里面。

python 复制代码
# 早高峰通勤决策
weather = input("今天天气如何(晴/雨/霾) >> ")
temperature = float(input("现在几度 >> "))
hurried = input("赶时间吗(y/n) >> ").lower() == "y"

# 顺序结构,一行接一行
print("出门前检查一遍")

# 选择结构,分情况讨论
if weather == "雨":
    print("带伞")
elif weather == "霾":
    print("戴口罩")
else:
    print("天不错,墨镜带上")

# 循环结构,重复到条件不满足为止
countdown = 3
while countdown > 0:
    print(f"还剩 {countdown} 分钟出发")
    countdown -= 1

# 又回到选择结构
if hurried or temperature < 0:
    print("打车走")
else:
    print("骑车走")

print("出门")

输入 雨、-2、n,输出是这样。

text 复制代码
出门前检查一遍
带伞
还剩 3 分钟出发
还剩 2 分钟出发
还剩 1 分钟出发
打车走
出门

顺序在最前面打底,中间穿插选择和循环,最后又是顺序。你以后写的每一个程序,拆到底都是这个骨架。


二、选择语句

2.1 最简单的 if

只关心一种情况时候用它。条件成立就执行块里的代码,不成立就当它不存在。

python 复制代码
age = int(input("你几岁 >> "))

if age >= 18:
    print("可以进网吧了")
    print("记得带身份证")

print("这句话无论如何都会打印")

输入 20,输出是这个样。

text 复制代码
可以进网吧了
记得带身份证
这句话无论如何都会打印

输入 15,if 那两块被整段跳过,只剩最后一行。

图上看,它就是一个判断框加一条往下的边。

markdown 复制代码
   条件成立?
   ┌───┴────┐
 真│        │假
   ▼        │
 ┌────────┐ │
 │ 执行块  │ │
 └───┬────┘ │
     └──┬───┘
        ▼
   后面的代码

2.2 两条路都得管,if-else

条件成立走一块,不成立走另一块,两条路必选其一。

python 复制代码
score = int(input("输入成绩 >> "))

if score >= 60:
    print("及格了")
else:
    print("准备补考")

print("查分结束")

运行结果取决于输入。输入 59 时走到 else,输入 60 时走到 if。不管走哪边,else 外面的 print("查分结束") 都照打不误。

这里有个新手常栽的坑,else 不能单独出现,它必须紧跟在同一个层次的 if 后面。写成这样就报错。

python 复制代码
# 错误示范
else:            # SyntaxError,找不到配对的 if
    print("打招呼")

另一个坑更容易被忽略,else 后面不能跟条件。想加条件请用 elif。

2.3 多分支,if-elif-else

三条以上的路就该请它出场。elif 是 else if 的缩写,可以串任意多个。

python 复制代码
score = int(input("输入成绩 >> "))

if score >= 90:
    grade = "优"
elif score >= 80:
    grade = "良"
elif score >= 70:
    grade = "中"
elif score >= 60:
    grade = "及格"
else:
    grade = "不及格"

print(f"等级是 {grade}")

执行的规矩只有一句话,从上往下挨个试,命中第一个就收工。后面的不看了。

markdown 复制代码
      ┌────────────┐
      │ score >= 90 │──真──► 优
      └──────┬─────┘
             │假
      ┌──────▼─────┐
      │ score >= 80 │──真──► 良
      └──────┬─────┘
             │假
      ┌──────▼─────┐
      │ score >= 70 │──真──► 中
      └──────┬─────┘
             │假
      ┌──────▼─────┐
      │ score >= 60 │──真──► 及格
      └──────┬─────┘
             │假
      ┌──────▼─────┐
      │   不及格     │
      └────────────┘

这里藏着一个经典错误。顺序写反,条件就废了。

python 复制代码
# 错误示范,顺序反了
if score >= 60:      # 85 分在这里就被拦下
    grade = "及格"
elif score >= 90:    # 这句永远走不到
    grade = "优"

85 分会命中第一个 >= 60,拿到「及格」。后面的 90 再也没机会。写多分支时养成习惯,把范围小的、严格的写在前面。反过来写,把宽松的放前面,等于给后面的条件判了死刑。

还有一种「看起来对其实漏了」的情况。

python 复制代码
score = 75

if score >= 60:
    grade = "及格"
elif score < 60:
    grade = "不及格"
# 看着覆盖了全部情况,但两种都不是的时候......

上面的写法其实穷尽了所有可能,没问题。但如果要处理的是字符串,比如 weather == "晴" 再 elif weather == "雨",输入「阴天」就两个都不命中。分支没有 else 兜底的时候,可能出现所有分支都不走的情况,变量就处于没定义的状态。所以我的习惯是,除非故意留空,多分支一律写一个 else。

2.4 嵌套选择

分支里面再开分支,用来表达「在满足某个前提的情况下继续细分」。

python 复制代码
score = int(input("输入成绩 >> "))

score = 95

if score >= 60:
    print("先庆祝一下")
    if score >= 90:
        print("居然是优")
    else:
        print("离优还差口气")
else:
    print("暑假泡汤了")

缩进层级在这里扮演了关键角色。

yaml 复制代码
if score >= 60:
    ┌──────────────────────────┐
    │ 第一层:及格了            │
    │   if score >= 90:        │
    │   ┌──────────────────┐   │
    │   │ 第二层:居然是优   │   │
    │   └──────────────────┘   │
    │   else:                  │
    │   ┌──────────────────┐   │
    │   │ 第二层:还差口气   │   │
    │   └──────────────────┘   │
    └──────────────────────────┘
else:
    ┌──────────────────────────┐
    │ 第一层:不及格            │
    └──────────────────────────┘

嵌套三层的代码我已经开始头疼了。超过三层就该考虑拆函数,或者把条件合并。

这两块代码效果完全一样,后者好读得多。

python 复制代码
weather = "晴"
temperature = 32
hurried = False

# 嵌套三层
if weather == "晴":
    if temperature > 30:
        if not hurried:
            print("骑车")
python 复制代码
weather = "晴"
temperature = 32
hurried = False

# 合并成一个条件
if weather == "晴" and temperature > 30 and not hurried:
    print("骑车")

我自己写代码的原则是,能用 and 拼起来就别嵌套。嵌套的唯一理由是内层的判断在外层成立之后才有意义,或者各层要执行的内容完全不同。

2.5 match-case(3.10 起)

Python 3.10 加了个新的分支写法,专门对付「跟一堆具体值做等值比较」的场景。先确认你的版本。

python 复制代码
import sys
print(sys.version_info)
# sys.version_info(major=3, minor=12, micro=0, releaselevel='final', serial=0)
# 只要 minor >= 10 就能用

把成绩那段改写成 match 是这副样子。

python 复制代码
level = int(input("输入等级数码(1-4) >> "))

match level:
    case 1:
        print("优秀")
    case 2:
        print("良好")
    case 3:
        print("及格")
    case 4:
        print("不及格")
    case _:
        print("输入有误")

最后那个 case _ 是通配符,等于 else,匹配任何前面没接住的值。位置必须放最后 ,放前面会把后面全挡住,道理和 elif 顺序一样。

match 的真正威力在于匹配结构,不只是匹配单个值。举几个我在实际项目里真用过的形态。

python 复制代码
def describe_http_code(code):
    match code:
        case 200 | 201 | 204:            # 或,多个值写一行
            return "成功"
        case 301 | 302:
            return "重定向"
        case 400 | 401 | 403:
            return "客户端问题"
        case 500 | 502 | 503:
            return "服务器挂了"
        case _:
            return "没见过这个码"

print(describe_http_code(200))   # 成功
print(describe_http_code(404))   # 没见过这个码
print(describe_http_code(503))   # 服务器挂了

还能顺手把值绑定出来。

python 复制代码
command = "add 5 3"
match command.split():
    case ["quit"]:
        print("退出")
    case ["add", x, y]:
        print(f"结果是 {int(x) + int(y)}")
    case ["hello", name]:
        print(f"你好 {name}")
    case _:
        print("不认识这个命令")

上面这段输入 "add 5 3" 会打印 结果是 8。x 和 y 自动接住了分割后的第二、第三个元素。写命令行小工具特别顺手。

那什么情况下用它,什么情况下用 if。我的判断是这个样子。

场景 用什么 理由
判断大小区间(score >= 90) if / elif match 擅长等值,区间写起来别扭
跟一堆具体常量比较(code == 200) match 一行一个值,扫一眼就会
判断布尔表达式组合 if match 在这儿纯属添乱
要把数据解包出来([add, x, y]) match 别的写法要手动切索引
需要兼容 Python 3.9 及以下 if 老版本没有 match

顺便说一句,match 不会替换 if。它就是个补充工具,专门处理那些写 if 时会出现一长串 elif x == 常量 的场合。这类代码改成 match 之后通常能短三分之一。

2.6 条件判断里的真真假假

if 后面跟的其实不一定是布尔值。Python 会把任何对象丢给 bool() 转一遍,看结果办事。这张表建议背下来。

类型 判为假 判为真
整数 0 其余全是真,包括 -1
浮点 0.0 其余全是真,包括 -0.0 之外的负数
字符串 "" 空串 其余全是真,包括 "0" 和 "False"
列表 / 元组 / 字典 / 集合 空的 非空全真,哪怕里面装一个 0
None None 不存在为真的情况

下面这几个是最容易咬人的,我在 REPL 里逐个试过。

python 复制代码
>>> bool("False")
True
>>> bool("0")
True
>>> bool([0])
True
>>> bool([])
False
>>> bool(0.0)
False

那个 bool("False") 等于 True 尤其要命。从配置文件读出来一个字符串 "False",拿去 if 判断,Python 觉得它是真家伙。我在这上面栽过不止一次。

判断的惯用写法,写成左边这样更地道。

新手写法 Python 的写法 说明
if len(items) > 0: if items: 直接判容器真假
if len(items) == 0: if not items: 反过来判空
if flag == True: if flag: 多此一举
if flag == False: if not flag: 同理
if name != None: if name is not None: 判 None 必须用 is
if type(x) == int: if isinstance(x, int): 后者认子类

判 None 那行单独说一句。None 在整个解释器里只有一个实例,用 is 比 == 更快也更准。有些对象的 __eq__ 是自己写的,== 可能返回奇怪的结果,is 不会。

2.7 if 的几个经典错误

这几条我基本全犯过,现在还偶尔手滑。

坑一,把 = 当成 ==。

python 复制代码
# 错误示范
if score = 60:        # SyntaxError: invalid syntax
    print("正好及格")

Python 不允许在条件里赋值,直接语法错误,算是好事。C 语言里这就变成了一个合法的比较意外为真的 bug,能查一天。Python 3.8 之后请直接用海象运算符 :=,意图明确。

python 复制代码
if (n := 10) > 5:
    print(f"n 得放到 {n}")       # 既赋值又比较

这写法我用得不多,偶尔在 while 读取流式数据时省事。

python 复制代码
while (line := input("输入内容(q 退出) >> ")) != "q":
    print(f"你说了 {line}")

坑二,拿浮点数做等值比较。

python 复制代码
>>> 0.1 + 0.2 == 0.3
False

第二章解释过原因,二进制浮点存不准十进制小数。要比较就比差值。

python 复制代码
a = 0.1 + 0.2
b = 0.3
if abs(a - b) < 1e-9:
    print("当成相等")

判据是「差得够小就算相等」,1e-9 是常用阈值。涉及金额请用 Decimal,第二章提过。

坑三,误用 is 比较具体值。

python 复制代码
a = 256
b = 256
print(a is b)        # True,但这是巧合

x = 257
y = 257
print(x is y)        # False,这下翻车了

is 比的是内存地址,不是值。Python 缓存了 -5 到 256 这些小整数,所以 256 看着「能用」,257 就露馅了。比较具体数值请永远用 ==。is 只留给 None、True、False 这类单例。

坑四,多个条件写散了。

python 复制代码
score = 75

# 啰嗦且容易漏逻辑
if score >= 60:
    if score < 80:
        print("中等")

写成一行更好。

python 复制代码
score = 75

if 60 <= score < 80:
    print("中等")

这种连着写的叫链式比较,是 Python 的特色语法。等价于此写法,但可读性高出一个档次。

python 复制代码
print(1 < 2 < 3)      # True
print(3 > 2 > 1)      # True
print(1 < 3 > 2)      # True,允许混着比较

坑五,把 input() 的字符串当数字比。

python 复制代码
age = input("你几岁 >> ")
print(type(age))      # <class 'str'>

if age >= 18:         # TypeError,字符串没法跟整数比
    print("成年")

正确做法是读进来先转类型。

python 复制代码
age = int(input("你几岁 >> "))
if age >= 18:
    print("成年")

字符串之间的比较虽然有结果,比的是字典序,"9" > "10" 结果是 True。第二章留过这个坑。

坑六,条件写成了死句。

python 复制代码
score = -5

if score > 100 and score < 0:      # 永远不可能同时成立
    print("数据出错")

想要「超出范围」应该用 or。

python 复制代码
score = 120

if score > 100 or score < 0:
    print("数据出错")

用链式写出来最清楚。

python 复制代码
score = 120

if not 0 <= score <= 100:
    print("数据出错")

三、条件表达式

3.1 一行写完的分支

有时候分支要做的事情特别简单,就是二选一给变量赋值。这种场合 Python 给了个紧凑写法,叫条件表达式,也有人叫它三元运算符。

python 复制代码
age = int(input("你几岁 >> "))

# 完整的 if-else 写法
if age >= 18:
    status = "成年"
else:
    status = "未成年"
print(status)

# 条件表达式,一行搞定
status = "成年" if age >= 18 else "未成年"
print(status)

语法长这个样子。

arduino 复制代码
变量 = 值1  if  条件  else  值2
        ▲              ▲
        │              │
     条件为真时取它     条件为假时取它

读法:先读中间的条件,真取左边,假取右边

注意中间的顺序。别的语言写 条件 ? 值1 : 值2,条件在最前面。Python 把条件放中间。刚上手那段日子我每次都要愣一下才能读顺。

3.2 它的执行顺序

这点很重要,条件表达式先算条件,再算其中一边。不会被选中那一边压根不执行。

python 复制代码
>>> age = 20
>>> "成年" if age >= 18 else "未成年"
'成年'

被跳过那边的副作用不会触发。写个带打印的验证一下。

python 复制代码
def left():
    print("左边算了")
    return "L"

def right():
    print("右边算了")
    return "R"

result = left() if True else right()
# 只打印「左边算了」

运行后终端只会出现 左边算了。这个特性有实际用处,把开销大的操作放到不会被选中的分支里,可以省时间。不过别指望太多,可读性比这点性能值钱。

3.3 嵌套三元,能别写就别写

条件表达式本身是可以套娃的。

python 复制代码
score = 85
grade = "优" if score >= 90 else "良" if score >= 80 else "不及格"
print(grade)      # 良

上面这行等价于串起来的 elif。读的时候从右往左劈开。

text 复制代码
# 拆开看
"优" if score >= 90 else ( "良" if score >= 80 else "不及格" )
        ▲                            ▲
        │                            └─ 假了再看这一坨
        └─ 真了取它

写起来确实短。但我写完半小时之后回头看自己都读不顺,真出问题时同事大概会顺着网线过来找我。我的建议是这个样子。

一层套用很舒服。

python 复制代码
member = True
raining = True
count = 3

discount = 0.8 if member else 1.0
tip = "带了雨伞" if raining else "什么都没带"
message = f"共 {count} 件" if count else "购物车是空的"

两层及以上请换回 if-elif-else,或者干脆用字典映射。

python 复制代码
# 可读性完胜嵌套三元
def get_grade(score):
    if score >= 90:
        return "优"
    elif score >= 80:
        return "良"
    elif score >= 60:
        return "及格"
    else:
        return "不及格"

3.4 跟 and/or 的暧昧关系

老一点的 Python 代码里常见这么一行。

python 复制代码
condition = True
value_a = 0
value_b = "默认"

result = condition and value_a or value_b
print(result)          # 默认,想要的是 0 却拿到了字符串

看着像条件表达式,其实不是一回事,而且有坑。

python 复制代码
>>> True and 0 or "默认"
'默认'

期望拿到 0,实际拿到字符串。0 本身是假值,and 之后结果为 0,or 发现 0 是假,就跑去取右边了。

用条件表达式就不会有这个问题。

python 复制代码
>>> 0 if True else "默认"
0

结论很简单,要二选一就用 value1 if condition else value2,别玩 and or 的花活。后者在今天唯一还能看的用处是给变量兜底取默认值,而且还得确认中间那截不会是假值。连这个我现在也改成用 if not 写清楚。

3.5 什么时候该用它

情况 选择
两个分支都是给同一个变量赋简单的值 条件表达式
分支里面有好几行代码要执行 完整 if-else
分支里要打印日志或者写文件 完整 if-else
需要三选一甚至更多 完整 if-elif-else
放在 f-string 里或者当函数参数 条件表达式挺合适

最后一行举个例子,这正是它最舒服的位置。

python 复制代码
count = int(input("购物车几件 >> "))
print(f"购物车{f'里有 {count} 件' if count else '是空的'}")

# 输入 3   → 购物车里有 3 件
# 输入 0   → 购物车是空的

这种场合写成 if 语句要多出四行,收益不划算。


四、循环语句

4.1 while 循环

记不住就记这三件事,初始条件、循环条件、更新变量。缺一个就是死循环。

python 复制代码
count = 0                  # ① 初始值

while count < 5:           # ② 循环条件
    print(f"第 {count + 1} 次问好")
    count += 1             # ③ 更新变量,千万别忘

print("循环结束")

输出是这个样。

text 复制代码
第 1 次问好
第 2 次问好
第 3 次问好
第 4 次问好
第 5 次问好
循环结束

流程图等价于是这个样子。

ini 复制代码
  count = 0              ← ①
     │
     ▼
 count < 5 ?  ──假──► 循环结束
     │真
     ▼
 打印一次
     │
 count += 1              ← ③
     │
     └──────► 回到上面再判 ②

把第 ③ 行删掉会发生什么。count 永远是 0,count < 5 永远成立,终端开始无限刷同一句话。这时候按住 Ctrl + C 强制中断,Windows 上偶尔得多按几次。

我自己写 while 的习惯是先把这三行骨架敲出来,再往中间填业务逻辑。这个顺序能保证不漏更新。

4.2 while 的几个典型场景

累加。

python 复制代码
total = 0
n = 1
while n <= 100:
    total += n
    n += 1
print(f"1 到 100 的和是 {total}")      # 5050

让用户反复输入直到给对为止。

python 复制代码
age = -1
while not (0 <= age <= 150):
    age = int(input("请输入合理年龄(0-150) >> "))
print(f"记录下来了,{age} 岁")

注意这里的初始化 age = -1,它的唯一作用是保证第一次能进循环。这种「哨兵值」的写法很常见。

不知道要转多少次的情况。

python 复制代码
import random

target = random.randint(1, 100)
guess = 0
times = 0

while guess != target:
    guess = int(input("猜个数(1-100) >> "))
    times += 1
    if guess < target:
        print("小了")
    elif guess > target:
        print("大了")

print(f"猜对了,用了 {times} 次")

第一章那个猜数字游戏就是这么写的。这种「不知道要循环几次」的场景是 while 的主场,for 干不了。

4.3 for-in 循环

Python 的 for 跟 C 语言那套 for(i=0;i<10;i++) 逻辑不一样。它是迭代器循环,直接从序列里挨个取元素。

python 复制代码
# 遍历字符串
for char in "Python":
    print(char)

输出是这个样。

text 复制代码
P
y
t
h
o
n

第一次用的时候我觉得有点怪。不用下标,不用 i++,直接对着元素干活。用习惯了会回不去。

配合 range() 就是最经典的计数循环。

python 复制代码
for i in range(5):
    print(f"第 {i + 1} 遍")

4.4 range 的三种形态

range 是 for 循环最铁的搭档,专门生成一串整数。它有三种写法。

写法 含义 产生的数
range(5) 从 0 到 5(不含 5) 0 1 2 3 4
range(2, 9) 从 2 到 9(不含 9) 2 3 4 5 6 7 8
range(1, 10, 2) 从 1 到 10,每次加 2 1 3 5 7 9
range(10, 0, -2) 从 10 到 0,每次减 2 10 8 6 4 2

核心口诀只有六个字,含头不含尾。终点自己取不到,最后一个数是它前面那位。

画成数轴特别清楚。

scss 复制代码
range(2, 9, 3)  →  2, 5, 8

   2    3    4    5    6    7    8    9
   ●────┼────┼────●────┼────┼────●────┆
   │         +3        +3         ┆
   起点                            终点(取不到)
   └─────────────────────────────────┘

几个必须记住的点。

python 复制代码
>>> list(range(5))
[0, 1, 2, 3, 4]
>>> list(range(0, 100, 3))[:5]
[0, 3, 6, 9, 12]
>>> len(range(0, 100, 3))
34
>>> list(range(10, 0, -2))
[10, 8, 6, 4, 2]
>>> list(range(0))
[]

最后那个 range(0) 返回空序列,for 一次都不执行。写代码的时候如果循环上限是算出来的,结果为负的情况要留意,别理所当然觉得至少跑一次。

还有个容易忽略的好处,range 是惰性的。它在 Python 3 里不真的生成列表,只在被问到的时候给下一个数。

python 复制代码
>>> range(10 ** 12)          # 一万个亿也不会卡
range(0, 1000000000000)
>>> len(range(10 ** 12))
1000000000000

换成 Python 2 或者 Java 的那种写法,内存当场就爆了。这也是为什么遍历整数序列请直接用 range,不要 list(range(...))。

4.5 遍历的实用技巧

带下标遍历用 enumerate。

新手常写这样,能跑但不地道。

python 复制代码
names = ["张三", "李四", "王五"]

# 新手常见写法,能跑但绕了一圈
for i in range(len(names)):
    print(i, names[i])

Python 的写法是这个样子。

python 复制代码
names = ["张三", "李四", "王五"]

# index 拿下标,name 拿元素,一次到位
for index, name in enumerate(names):
    print(index, name)

还可以指定起始编号。

python 复制代码
names = ["张三", "李四", "王五"]

for index, name in enumerate(names, start=1):
    print(f"{index}. {name}")

输出是这个样。

text 复制代码
1. 张三
2. 李四
3. 王五

打印排行榜、菜单清单这类要编号的场景,几乎每次都用。

两个列表拉链式对齐用 zip。

python 复制代码
names = ["张三", "李四", "王五"]
scores = [88, 92, 79]

for name, score in zip(names, scores):
    print(f"{name} 考了 {score} 分")

有个陷阱,长度不一致时以短的为准,多出来的直接丢掉。

python 复制代码
>>> list(zip([1, 2, 3], "ab"))
[(1, 'a'), (2, 'b')]

那个 3 默默消失了。要是多出来的数据很重要,请在循环之前先检查一下长度,或者用 itertools.zip_longest。

倒着遍历用 reversed。

python 复制代码
for i in reversed(range(1, 6)):
    print(i)          # 5 4 3 2 1

或者直接用 range 的负步长,效果一样。我两种都用,看当时哪个先想起来。

4.6 while True 惯用法

这是个看着反常识但极其常见的写法。条件写成常量 True,循环体内部用 break 跳出去。

python 复制代码
while True:
    command = input("输入命令(q 退出) >> ")
    if command == "q":
        break
    print(f"执行命令 {command}")

这么做的好处是,退出条件可以写在循环体的任意位置,不止偏偏写在开头。第一章那个猜数字游戏就用了这个套路。

对比一下传统写法。

python 复制代码
# 传统写法,退出条件只能写在头部
command = ""
while command != "q":
    command = input("输入命令(q 退出) >> ")
    if command != "q":
        print(f"执行命令 {command}")

上面那段有个明显的瑕疵,判断重复写了两次,还得在循环前面初始化一个空字符串。while True 版本干净得多。

我写代码时的判断标准是,如果退出条件在循环体中间或者末尾,用 while True;如果退出条件天然在开头且条件简单,用普通 while。

4.7 嵌套循环

循环里面套循环,用来处理二维的东西,比如乘法表、矩阵、多重目录下的文件。

python 复制代码
for i in range(1, 4):
    for j in range(1, 4):
        print(f"i={i}, j={j}")

执行顺序搞清楚很重要,外层走一步,内层跑一圈。

ini 复制代码
i=1  ──►  j=1, 2, 3   (内层跑完三轮)
i=2  ──►  j=1, 2, 3
i=3  ──►  j=1, 2, 3

输出就摆在那儿。

text 复制代码
i=1, j=1
i=1, j=2
i=1, j=3
i=2, j=1
i=2, j=2
i=2, j=3
i=3, j=1
i=3, j=2
i=3, j=3

总共 3 乘 3 等于 9 次。循环层数每加一层,总次数就是乘法。三层各 100 次的一百万次循环,Python 跑起来肉眼能感到停顿。层数多的时候要想想有没有更省的办法。

4.8 循环的 else 子句

这是 Python 独有的设计,while 和 for 都可以带一个 else。它的语义特别反直觉,我第一次看到以为自己理解错了。

else 里的代码在循环「正常结束」的时候执行。 被 break 打断的不算正常结束。

python 复制代码
for item in [1, 2, 3]:
    print(item)
else:
    print("全部走完,没有中途退出")
text 复制代码
1
2
3
全部走完,没有中途退出

加上 break 看看。

python 复制代码
for item in [1, 2, 3]:
    if item == 2:
        break
    print(item)
else:
    print("这句不会打印")

只输出一个 1。break 相当于给这段程序判了 「非正常结束」,else 就被跳过了。

kotlin 复制代码
        ┌──────────────┐
        │ for / while  │
        └──────┬───────┘
               │
          ┌────▼─────┐
          │ 循环体    │
          └────┬─────┘
               │
      break ───┼─── 正常跑完
       ┌───────┴──────┐
       ▼              ▼
  ┌─────────┐   ┌──────────┐
  │跳过 else │   │执行 else  │
  └─────────┘   └──────────┘

这东西最经典的用途是「找了一圈没找到」。素数判断就是标准例子。

python 复制代码
n = 97
for i in range(2, int(n ** 0.5) + 1):
    if n % i == 0:
        print(f"{n} 不是素数,能被 {i} 整除")
        break
else:
    print(f"{n} 是素数")

for 跑完所有可能的因子都没撞上整除,走 else 宣布它是素数。撞上了就 break,else 自动跳过。比起用标志变量记账干净太多。

不用 else 的写法是这个样,多一个变量要维护。

python 复制代码
n = 97
is_prime = True
for i in range(2, int(n ** 0.5) + 1):
    if n % i == 0:
        is_prime = False
        break
if is_prime:
    print(f"{n} 是素数")

顺带一提,空序列也会触发 else。这一点经常搞得人一愣。

python 复制代码
for x in []:
    print("不会执行")
else:
    print("空序列也会走 else")      # 会打印

原理还是一样的,循环没有被 break,它就是正常跑完了,只不过一轮都没跑。记住这点,避免以后写筛查逻辑时出错。

4.9 循环性能的几个提醒

新手容易在循环里面干这些事,慢得很隐蔽。

别用加法拼字符串。

python 复制代码
# 慢,每次 += 都新建字符串对象
result = ""
for i in range(10000):
    result += str(i)

# 快,先用列表收集,最后一次性 join
parts = []
for i in range(10000):
    parts.append(str(i))
result = "".join(parts)

数据量小的时候都行。超过几千次循环的场合,后面这种写法能快上一个数量级。

别在循环里重复算同样的东西。

python 复制代码
data = [10, 20, 30, 40, 50]

# 每次循环都要算一遍长度
for i in range(len(data)):
    data[i] = data[i] * 2

# 提前算好,长度只求一次
size = len(data)
for i in range(size):
    data[i] = data[i] + 1

严格说 len() 本身是 O(1),几乎没有开销。但如果是函数调用或者表达式,提出来明显更划算。

循环体里别反复做全局查找。 这条等讲到函数作用域再展开,现在先知道有这回事。


五、跳转语句

5.1 break,整个循环停下

break 一旦命中,它所在的那层循环立即结束,不管后面还有几轮没跑。

python 复制代码
for i in range(10):
    if i == 5:
        break
    print(i)
print("循环外")

# 输出 0 1 2 3 4 然后跳出

图示是这样。

ini 复制代码
i=0  打印
i=1  打印
i=2  打印
i=3  打印
i=4  打印
i=5  ──► 命中 break,整个 for 报废,直接跳到循环外

典型的用法是「找到了就收工」。

python 复制代码
names = ["张三", "李四", "王五", "赵六"]

for name in names:
    if name == "王五":
        print(f"在第 {names.index(name) + 1} 个位置找到了")
        break
else:
    print("名单里没有这个人")

配 else 用起来很顺,命中就是 break 加处理,落空就是 else 兜底。

5.2 continue,只跳过这一轮

continue 温和得多。它跳过的是本轮剩余的代码,然后回到循环头部继续下一轮,循环本身不停。

python 复制代码
for i in range(10):
    if i % 2 == 0:
        continue
    print(i)
print("循环外")

输出只剩奇数。

text 复制代码
1
3
5
7
9
循环外

两者的区别画出来就很清楚。

go 复制代码
for i in range(5):
    if i == 2: break
    print(i)

   0  ─ 打印
   1  ─ 打印
   2  ─ 命中 break ──► 循环结束(外面继续)
  结果 0 1


for i in range(5):
    if i == 2: continue
    print(i)

   0  ─ 打印
   1  ─ 打印
   2  ─ 命中 continue ──► 跳过 print,回头部拿下一个 i
   3  ─ 打印
   4  ─ 打印
  结果 0 1 3 4

continue 的正确使用场景,一是跳过不符合条件的数据,二是跳过可能引发错误的输入。

python 复制代码
# 只处理正数,负数跳过
numbers = [3, -1, 5, -7, 9, 0, 2]

total = 0
for n in numbers:
    if n <= 0:
        continue          # 负数和 0 都不参与累加
    total += n

print(f"正数之和 {total}")      # 19

想不看两遍验证一下这个数,3 加 5 加 9 加 2 确实等于 19。

5.3 continue 在 while 里的陷阱

这条单独拎出来说,因为它几乎必然导致死循环。

python 复制代码
# 错误示范,会死循环
count = 0
while count < 5:
    if count == 2:
        continue          # 回到头部,但 count 没更新,还是 2
    print(count)
    count += 1            # 这行被 continue 跳过了

continue 直接跳回循环头,那句 count += 1 永远轮不到。count 卡死在 2,终端无限刷屏。按 Ctrl + C 停。

修法很简单,把更新提到 continue 之前,或者改用计数循环。

python 复制代码
count = 0
while count < 5:
    count += 1            # 先更新
    if count == 2:
        continue
    print(count)

更彻底的办法是直接用 for。for 循环的迭代由 Python 托管,跳过了不会漏更新,天生免疫这个坑。

python 复制代码
for count in range(5):
    if count == 2:
        continue
    print(count)

我的习惯是能用 for 就不用 while。要循环次数已知,for 是更安全的选项。

5.4 跳出多层循环

这是个真实存在的麻烦。break 只管它自己那一层,外层照跑。

python 复制代码
for i in range(3):
    for j in range(3):
        if j == 1:
            break          # 只跳内层
    print(f"外层还在走,i={i}")

外层那三轮一次不少。想在命中时同时停外层,常见的三种办法。

方案一,标志变量。 兼容性最好,任何语言都能这么写。

python 复制代码
# 在 10x10 的棋盘里找 i * j == 42 的位置
found = False
for i in range(10):
    for j in range(10):
        if i * j == 42:
            found = True
            break
    if found:
        break

print(f"找到 i={i}, j={j}")      # 找到 i=6, j=7

方案二,包一层函数用 return。 我个人最推荐这个。

python 复制代码
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]
target = 6

def find_position(matrix, target):
    for i, row in enumerate(matrix):
        for j, value in enumerate(row):
            if value == target:
                return (i, j)          # return 直接离开整个函数
    return None

position = find_position(matrix, target)
print(position)          # (1, 2)

return 能把控制权一口气交还给调用者,几层循环都跳得出来。这也是为什么「看到嵌套三层以上的循环」,我的第一反应是把内层抽成函数。

方案三,用异常。 能解决但是大材小用,日常业务代码里我不建议。知道有这么回事就行。

5.5 循环外面还有活干的时候

break 出来之后通常还要做点收尾。这种时候 else 或者标志变量就派上用场了。

python 复制代码
numbers = [2, 4, 6, 9, 10]

for n in numbers:
    if n % 2 == 1:
        print(f"找到奇数 {n}")
        break
else:
    print("全是偶数")

输出 找到奇数 9。把列表里的 9 改成 8,输出变成 全是偶数。

这种写法比起设标志变量少了两行,逻辑也更集中在循环附近。我近两年写筛查代码基本都用它。


六、pass 空语句

6.1 它是个占位符

pass 是 Python 里唯一一条什么都不做的语句。它的用处很朴素,让解释器别再追问这块代码到底干了什么。

python 复制代码
score = 87

if score >= 60:
    pass          # 什么都不做,但语法上必须有东西
else:
    print("补考")

把 pass 删掉会怎样。

python 复制代码
# 错误示范
if score >= 60:
                 # IndentationError: expected an indented block
else:
    print("补考")

Python 不允许一个块是空的。缩进开始了就必须有内容,pass 就是「这里有内容,内容是什么都不干」的意思。

6.2 跟注释不一样

这一点特别容易被弄混。

python 复制代码
if condition:
    # 以后再处理           ← 注释不算语句,依然报错
else:
    do_something()

注释在语法层面完全不存在。解释器看那一堆,只看到一个空空洞洞。所以想要「先留个坑」,得写 pass,或者两者都写。

python 复制代码
condition = True

if condition:
    pass          # TODO 这里将来要加日志上报
else:
    do_something()

我写代码时的习惯是 pass 加一行 TODO 注释。这样除了解释器不报错,IDE 和代码审查工具也能把 TODO 扫出来,不容易忘掉。

6.3 三个我在生产代码里真用它的地方

其一,占坑。搭骨架的时候先把壳搭好。

python 复制代码
def load_config(path):
    pass          # 明天再说

def save_result(data):
    pass

def generate_report(data):
    pass

这么写是为了先让整个文件能被导入、能被测试跑通,函数体留着慢慢填。比写半成品逻辑安全得多。

其二,吞掉不关心的异常。

python 复制代码
import os

try:
    os.remove("temp.lock")
except FileNotFoundError:
    pass          # 文件本来就不存在,删不掉正好

这种「我知道它可能报错,而且我不在乎」的场合,pass 是标准答案。但要小心别滥用,把所有异常都 pass 掉等于给程序埋地雷,出问题时完全没线索。我的做法是至少加个注释说明为什么可以不管。

其三,定义最小的类或者异常类型。

python 复制代码
class MyError(Exception):
    pass          # 继承 Exception 就够了,不需要新行为

class StatusFlag:
    pass          # 纯粹当标记用的空类

自定义异常类型时几乎人人都这么写。继承来的行为已经够用,强行加内容反而是画蛇添足。

6.4 一个替代品,...

除了 pass,三个点也能当占位。它是 Python 内置的 Ellipsis 对象。

python 复制代码
def todo():
    ...

class Placeholder:
    ...

两种写法功能上等价。我看社区的项目,... 在类型注解的存根文件里更常见,普通业务代码还是 pass 多一些。选一个用就行,别在一个项目里混着来。

6.5 别混淆这几个「跳过」

这张表建议对着看两遍,pass、continue、break、return 经常被新手搞串。

关键字 作用在 效果
pass 语法占位 什么都不做,继续往下执行
continue 当前轮循环 跳过本轮剩余代码,回循环头拿下一个
break 当前层的整个循环 循环报废,跳到循环外第一行
return 当前函数 带着返回值离开函数,几层循环都拦不住

最后一个比喻帮我理清了很久,pass 是「这轮啥也不干继续走」,continue 是「这轮跳过我快进下一轮」,break 是「这片到此为止」,return 是「整个函数就这样吧」。


七、实战

7.1 实战一,成绩评级(三种写法)

需求,输入成绩(0 到 100),输出优、良、中、及格、不及格,同时给出对应的 GPA。

版本一,经典 if-elif-else。

python 复制代码
# grade_report.py
score = int(input("输入成绩(0-100) >> "))

if not 0 <= score <= 100:
    print("成绩超出范围")
else:
    if score >= 90:
        grade, point = "优", 4.0
    elif score >= 80:
        grade, point = "良", 3.0
    elif score >= 70:
        grade, point = "中", 2.0
    elif score >= 60:
        grade, point = "及格", 1.0
    else:
        grade, point = "不及格", 0.0
    print(f"等级 {grade},绩点 {point}")

注意开头那句范围校验,not 0 <= score <= 100 用了链式比较加取反。输入 1000 会在这里被拦住,不往下走。Python 里可以直接这么写,别的语言得拆成两句。

实跑的输入输出。

text 复制代码
输入成绩(0-100) >> 87
等级 良,绩点 3.0

输入成绩(0-100) >> 55
等级 不及格,绩点 0.0

输入成绩(0-100) >> 1000
成绩超出范围

版本二,用 match 改写。

思路是把分数折算成十位数的档位,再对档位做匹配。

python 复制代码
# grade_match.py
score = int(input("输入成绩(0-100) >> "))

if not 0 <= score <= 100:
    print("成绩超出范围")
else:
    match score // 10:
        case 10 | 9:
            grade, point = "优", 4.0
        case 8:
            grade, point = "良", 3.0
        case 7:
            grade, point = "中", 2.0
        case 6:
            grade, point = "及格", 1.0
        case _:
            grade, point = "不及格", 0.0
    print(f"等级 {grade},绩点 {point}")

输入 87 输出 等级 良,绩点 3.0,跟上面一致。case 10 | 9 处理满分和九十几分两种。

版本三,字典加循环。 这个写法扩展性最好,规则变了只改配置。

python 复制代码
# grade_table.py
rules = [
    (90, "优", 4.0),
    (80, "良", 3.0),
    (70, "中", 2.0),
    (60, "及格", 1.0),
    (0,  "不及格", 0.0),
]

score = int(input("输入成绩(0-100) >> "))

if 0 <= score <= 100:
    for threshold, grade, point in rules:
        if score >= threshold:
            print(f"等级 {grade},绩点 {point}")
            break
else:
    print("成绩超出范围")

三种写法都能跑出正确结果。真到项目里我大概率选第三种,因为等级规则常年变动,写在列表里改起来不动业务逻辑。

7.2 实战二,猜数字(升级版)

需求,系统随机出个 1 到 100 的数,玩家最多猜 7 次,每次提示大小,猜完可以再来一局。输入非法内容不能崩。

python 复制代码
# guess_number.py
import random

print("=" * 36)
print("        猜 数 字 小 游 戏")
print("        范围 1 ~ 100,共 7 次机会")
print("=" * 36)

while True:
    answer = random.randint(1, 100)
    max_tries = 7
    used = 0

    while used < max_tries:
        raw = input(f"第 {used + 1}/{max_tries} 次,请输入 >> ")

        if raw.lower() == "q":
            print("这局弃权")
            break

        if not raw.isdigit():
            print("得输入正整数,或者按 q 退出")
            continue

        guess = int(raw)
        if not 1 <= guess <= 100:
            print("超出范围了,只在 1 到 100 之间猜")
            continue

        used += 1

        if guess == answer:
            print(f"猜对了!答案就是 {answer},用了 {used} 次")
            break
        elif guess < answer:
            print("小了")
        else:
            print("大了")

        if used == max_tries:
            print(f"机会用完了,答案是 {answer}")
    else:
        pass          # while 正常结束意味着用完机会,上面的 if 已经处理

    again = input("再来一局?(y/n) >> ").lower()
    if again != "y":
        print("游戏结束,下次再玩")
        break

我实跑了一局,输入 50、25、12、18、22、20、19,中间穿插一个 abc 看校验。输出是这样。

text 复制代码
====================================
        猜 数 字 小 游 戏
        范围 1 ~ 100,共 7 次机会
====================================
第 1/7 次,请输入 >> 50
大了
第 2/7 次,请输入 >> 25
小了
第 3/7 次,请输入 >> abc
得输入正整数,或者按 q 退出
第 3/7 次,请输入 >> 40
大了
第 4/7 次,请输入 >> 33
小了
第 5/7 次,请输入 >> 36
小了
第 6/7 次,请输入 >> 38
猜对了!答案就是 38,用了 6 次
再来一局?(y/n) >> n
游戏结束,下次再玩

这里用到的知识点攒了一堆,逐个说一下。

外层 while True 控制「玩几局」,内层 while used < max_tries 控制「每局几次机会」,两层循环配合 break 各管一摊。

raw.isdigit() 做输入校验。这里有个细节,isdigit() 只认非负整数字符串,"-3" 和 "12.5" 都会被判为假。正好符合需求。

注意 continue 的位置。非法输入时走到 continue,这次尝试不算,次数不增加。我最初把 used += 1 写在了校验之前,结果输了三次乱码就把机会耗光了,调试了半天才发现。

7.3 实战三,九九乘法表

需求,打印标准九九乘法表,四个角标号,等号右边对齐。

python 复制代码
# multiplication_table.py
print("          九 九 乘 法 表")
print("-" * 46)

for i in range(1, 10):
    for j in range(1, i + 1):
        product = i * j
        tail = "  " if j < i else ""       # 行尾不再多打空格
        print(f"{j}×{i}={product:<2}", end=tail)
    print()                                # 一行打完才换行

print("-" * 46)

实跑输出是这样。

text 复制代码
          九 九 乘 法 表
----------------------------------------------
1×1=1
1×2=2   2×2=4
1×3=3   2×3=6   3×3=9
1×4=4   2×4=8   3×4=12  4×4=16
1×5=5   2×5=10  3×5=15  4×5=20  5×5=25
1×6=6   2×6=12  3×6=18  4×6=24  5×6=30  6×6=36
1×7=7   2×7=14  3×7=21  4×7=28  5×7=35  6×7=42  7×7=49
1×8=8   2×8=16  3×8=24  4×8=32  5×8=40  6×8=48  7×8=56  8×8=64
1×9=9   2×9=18  3×9=27  4×9=36  5×9=45  6×9=54  7×9=63  8×9=72  9×9=81
----------------------------------------------

三个细节值得说一下。

for j in range(1, i + 1) 控制每行只打到对角线。第 i 行有 i 列,这就是三角形状的来源。

f"{j}×{i}={product:<2}" 里那个 <2 是左对齐占两位。乘积是一位数时补个空格,两位数时原样输出。少了它,第二行会变成 1×2=2 2×2=4 而第三行变成 1×3=3 2×3=6 3×3=9,参差不齐。这个格式符第二章讲过。

内层 print(..., end=" ") 把默认的换行改成空格,让同一行的算式排在一起。内层跑完之后那句单独的 print() 负责换行。这个搭配几乎是打印表格的定式。

顺手给个加餐,打印右上三角试试。

python 复制代码
for i in range(1, 10):
    for j in range(1, i):
        print("       ", end=" ")       # 先垫空格
    for j in range(i, 10):
        print(f"{i}×{j}={i * j:<2}", end=" ")
    print()

7.4 实战四,素数判定与筛选

需求 ,判断一个数是不是素数,然后打印 100 以内的全部素数。这里用 4.8 节讲的 else 子句。

python 复制代码
# prime_check.py
n = int(input("输入一个大于 1 的整数 >> "))

if n < 2:
    print(f"{n} 不是素数")
else:
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            print(f"{n} 不是素数,能被 {i} 整除")
            break
    else:
        print(f"{n} 是素数")

实跑的结果。

text 复制代码
输入一个大于 1 的整数 >> 97
97 是素数

输入一个大于 1 的整数 >> 91
91 不是素数,能被 7 整除

输入一个大于 1 的整数 >> 1
1 不是素数

那个 int(n ** 0.5) + 1 里藏着个优化。判断 n 是不是素数,其实不用试到 n-1,试到根号 n 就够了。理由很简单,如果 n 有个大于根号 n 的因子 k,那必定存在另一个小于根号 n 的因子 n/k,早就被试出来了。判断 10000 以内的数,这一句能把循环从一万次压到一百次。

再写个批量筛选的版本,顺便把 for-else 用起来。

python 复制代码
# prime_list.py
primes = []

for n in range(2, 101):
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            break
    else:
        primes.append(n)

print(f"100 以内共有 {len(primes)} 个素数")
print(primes)

实跑输出是这样。

text 复制代码
100 以内共有 25 个素数
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

那段输出我在 REPL 里核对过,2 到 100 之间确实是 25 个素数,跟我脑子里记的数目对得上。

7.5 实战五,命令行菜单系统

需求 ,一个能反复操作的菜单,支持查询余额、存款、取款、退出。这是综合性最强的一个,while True、match、continue、break 全上场。

python 复制代码
# atm_menu.py
balance = 1000.0

MENU = """
┌────────────────────────┐
│      简易银行系统       │
├────────────────────────┤
│   1. 查询余额           │
│   2. 存款               │
│   3. 取款               │
│   4. 退出               │
└────────────────────────┘
"""

while True:
    print(MENU)
    choice = input("请选择操作(1-4) >> ").strip()

    match choice:
        case "1":
            print(f"当前余额 ¥{balance:.2f}")

        case "2":
            raw = input("存入金额 >> ")
            if not raw.replace(".", "", 1).isdigit():
                print("金额得是数字")
                continue
            amount = float(raw)
            if amount <= 0:
                print("存款金额必须大于 0")
                continue
            balance += amount
            print(f"存入 ¥{amount:.2f},余额 ¥{balance:.2f}")

        case "3":
            raw = input("取出金额 >> ")
            if not raw.replace(".", "", 1).isdigit():
                print("金额得是数字")
                continue
            amount = float(raw)
            if amount <= 0:
                print("取款金额必须大于 0")
                continue
            if amount > balance:
                print("余额不足")
                continue
            balance -= amount
            print(f"取出 ¥{amount:.2f},余额 ¥{balance:.2f}")

        case "4":
            print("感谢使用,再见")
            break                       # match 不是循环,break 作用于外层 while

        case _:
            print("没有这个选项,请输入 1 到 4")

    input("按回车键继续...")
    print()

实跑一段对话。

text 复制代码
┌────────────────────────┐
│      简易银行系统       │
├────────────────────────┤
│   1. 查询余额           │
│   2. 存款               │
│   3. 取款               │
│   4. 退出               │
└────────────────────────┘
请选择操作(1-4) >> 1
当前余额 ¥1000.00
按回车键继续...

(中间省略重复的菜单)

请选择操作(1-4) >> 3
取出金额 >> 1500
余额不足

(这里的"按回车键继续"被 continue 跳过了,直接回到菜单)

请选择操作(1-4) >> 2
存入金额 >> 500
存入 ¥500.00,余额 ¥1500.00
按回车键继续...

请选择操作(1-4) >> 4
感谢使用,再见

几个设计选择值得说一下。

菜单用三引号字符串一次性准备好,每次循环直接 print(MENU)。比起每次重画一遍清爽很多。

金额校验那句 raw.replace(".", "", 1).isdigit() 有点意思。它把第一个小数点去掉,剩下的如果是纯数字就认为金额合法。12.5 变成 125 通过,12.5.5 变成 125.5 不通过。虽然粗糙了点,对付日常输入足够了。真要做金融系统请上 Decimal。

每一轮的分支里我都用 continue 而不是 break 来处理非法输入。非法输入要的是「跳过本轮剩下的处理,回到菜单」,continue 正合适。前面那段对话里「余额不足」之后没有出现「按回车键继续」,就是被 continue 跳过了,这个细节可以对照着看。

退出用的是 break。这里有个容易搞混的点,match 本身不是循环,break 会往上找最近的循环体,正好是外层那个 while True。所以写在 case "4" 里的 break 能把整个菜单系统停下来。

7.6 补一段,第二章那张小票重写

第二章预告过要把小票用循环重写,兑现一下。

原来的写法每种商品抄一行,goods 有几种就得抄几行。现在改成数据住在一个列表里,循环负责打印。

python 复制代码
# receipt_v2.py
shop_name = "街角便利店"
discount_rate = 0.9

goods = [
    ("牛奶", 3.5, 2),
    ("面包", 8.0, 1),
    ("咖啡", 12.8, 3),
    ("薯片", 6.5, 4),
    ("矿泉水", 2.0, 6),
]

total = 0.0

print(f"{shop_name:^30}")
print("-" * 42)
print(f"{'商品':<8}{'单价':>8}{'数量':>8}{'小计':>12}")
print("-" * 42)

for name, price, count in goods:
    subtotal = price * count
    total += subtotal
    print(f"{name:<8}{price:>8.2f}{count:>8}{subtotal:>12.2f}")

payable = total * discount_rate

print("-" * 42)
print(f"{'合计':<8}{'':>8}{'':>8}{total:>12.2f}")
print(f"{'折扣':<8}{'':>8}{'':>8}{'9折':>12}")
print(f"{'应付':<8}{'':>8}{'':>8}{payable:>12.2f}")
print("-" * 42)
print(f"{'谢谢惠顾,欢迎下次光临':^30}")

实跑输出是这样。

text 复制代码
            街角便利店
------------------------------------------
商品            单价      数量          小计
------------------------------------------
牛奶          3.50       2        7.00
面包          8.00       1        8.00
咖啡         12.80       3       38.40
薯片          6.50       4       26.00
矿泉水         2.00       6       12.00
------------------------------------------
合计                             91.40
折扣                                9折
应付                             82.26
------------------------------------------
        谢谢惠顾,欢迎下次光临

改起来有多轻松,现在加一百种商品,只需要往 goods 列表里加数据,print 那一坨一个字都不用动。这就是循环带来的好处,把重复的动作关进一段代码里,数据自己流动。


八、小结

8.1 本章知识清单

主题 关键点
三种结构 顺序、选择、循环,都单入口单出口
选择语句 if / if-else / if-elif-else / match-case,从上往下命中即停
跳过的坑 = 与 ==、浮点等值、is 与 ==、链式比较、input 类型转换
条件表达式 值1 if 条件 else 值2,条件在中间,一层最香两层危险
while 循环 初始、条件、更新三件套,忘了更新就死循环
for 循环 遍历序列为主,配合 range 做计数
range 含头不含尾,惰性求值,三种参数形态
遍历技巧 enumerate 带编号、zip 拉对齐、reversed 倒着走
while True 退出条件写在循环体内部,配 break 用
循环的 else 正常跑完才执行,被 break 打断就不执行
break 干掉当前层整个循环
continue 只跳过本轮剩余部分
多层跳出 标志变量、包函数用 return
pass 语法占位,什么都不做但必须有

8.2 避坑清单

这一章埋的雷比前两章都多,列出来对照检查。

  1. if 条件里写 = 而不是 ==,语法错误,这是好事,C 语言里它会静默通过然后算错。
  2. 浮点数用 == 比较,请改判 abs(a - b) < 1e-9,算钱上 Decimal。
  3. 拿 is 比较具体数值,Python 只缓存了 -5 到 256,257 开始就翻车。
  4. elif 顺序写宽了再写严的,前面的条件会把后面的挡死。
  5. while 循环忘了更新变量,终端开始无限刷新,按 Ctrl + C 救场。
  6. while 里用 continue 却把更新写在它后面,continue 一跳转就永远不执行更新。
  7. input() 读进来是字符串,直接跟数字比会 TypeError,先 int() 或 float()。
  8. match 的 case _ 写在了最前面,把后面所有分支全挡住。
  9. break 想跳两层,实际只跳了最里面那层。
  10. 分支结构忘了写 else 兜底,变量在某些输入路径下压根没被赋值。
  11. 嵌套三元写在生产代码里,三天后自己也读不懂。
  12. 用 + 在循环里拼大字符串,慢得让你怀疑 CPU 是不是坏了。

8.3 课后练习

  1. 输入一个年份,判断是不是闰年。规则是能被 4 整除且不能被 100 整除,或者能被 400 整除。拿 2000、1900、2024 三个数验证。
  2. 输入三个整数,用分支找出最大的那个输出。不许直接调用 max()。
  3. 打印 1 到 100 之间所有能被 7 整除的数,每行五个,用字元控制换行时机。
  4. 输入一个正整数 n,计算 1 到 n 的累加和。要求同时用 for 写一遍、用 while 写一遍,体会两者的差别。
  5. 挑战题,输入一个正整数,判断它是不是回文数(正反读一样,比如 1221、3443)。提示是把数字转成字符串之后用切片反转,想想 [::-1]。

8.4 下一章

第四章讲列表与元组 ,也就是真正意义上的容器。这一章写的 goods 列表你还只是往里塞数据、用循环读出来,下一章要解决的是怎么动态地加元素、删元素、切片取一部分、排序、以及列表推导式这种能一行写完循环的写法。


附录|流程控制速查卡

python 复制代码
# 选择
if 条件:
    ...
elif 条件:
    ...
else:
    ...

# 条件表达式
值 = A if 条件 else B

# match(3.10+)
match 变量:
    case 常量1 | 常量2:
        ...
    case _:
        ...

# while
while 条件:
    ...
else:
    ...              # 没被 break 才执行

# for
for 元素 in 序列:
    ...
else:
    ...

# range
range(终点)              # 0 到 终点-1
range(起点, 终点)         # 起点 到 终点-1
range(起点, 终点, 步长)   # 步长可以是负数

# 带编号遍历
for i, v in enumerate(序列, start=1):
    ...

# 拉链遍历
for a, b in zip(序列1, 序列2):
    ...

# 跳转
break                   # 结束本层循环
continue                # 跳过本轮剩余
pass                    # 什么都不做

# 惯用法
while True:
    if 满足条件:
        break

这张表跟第二章那张放一起,前三章的语法基本就齐了。

相关推荐
旖旎夜光6 小时前
【LangGraph实战】LangGraph 学习笔记(四):持久化——从线程记忆到跨会话长期记忆
人工智能·笔记·python·学习·ai编程·langgraph
yeflx6 小时前
万向节死锁
python
亿道电子Emdoor6 小时前
【Perforce】Klocwork-kwgcheck图形化界面无法打开如何解决
git·python·github
迅猛龙办公室8 小时前
python实现倒序输出数字
开发语言·python
全栈练习生17 小时前
AI Agent 沙箱
python·ai
SEO_juper19 小时前
用 Python 写一个 GEO 可见性检查脚本:你的网站现在能被 AI 引用吗
开发语言·人工智能·爬虫·python·seo·外贸独立站
用户83562907805120 小时前
使用 Python 对 Excel 工作表进行排序
后端·python
for_ever_love__20 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
用户83562907805120 小时前
使用 Python 合并 PowerPoint 演示文稿
后端·python
weixin_4617694020 小时前
VS Code 中创建 Jupyter 文件(.ipynb)
ide·python·jupyter