Day 010 — 循环(for/while)
"循环让计算机从重复劳动中解放——这恰恰是编程最本质的价值"
目录
- 为什么需要循环?
- for 循环与迭代器原理
- range() 函数详解
- break / continue / else 控制流
- while 循环与无限循环
- 循环嵌套
- 实战:九九乘法表
- 实战:菱形打印
- 思考题
1. 为什么需要循环?
1.1 没有循环的世界
假设你要打印 1 到 5 的数字:
print(1)
print(2)
print(3)
print(4)
print(5)
如果要打印 1 到 1000 呢?手写 1000 行 print()?这就是循环要解决的问题。
1.2 循环的本质
循环(Loop) 的核心思想是:同一段逻辑,作用于不同的数据。
┌─────────────────────────────────────────────────┐
│ 循环的本质 │
│ │
│ 数据流: [a, b, c, d, ...] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ x = 数据流中下一个元素 │ │
│ │ 对 x 执行统一操作 │ │
│ │ 继续下一个 │ │
│ └─────────────────────────────┘ │
│ │ │
│ ▼ │
│ 结果: 处理(a), 处理(b), 处理(c), ... │
└─────────────────────────────────────────────────┘
1.3 循环 = 迭代器 + 条件判断
Python 中几乎所有循环都可以理解为:
循环 = 迭代器(数据来源)+ 条件判断(何时停止)+ 循环体(做什么)
2. for 循环与迭代器原理
2.1 基本语法
for 变量 in 可迭代对象:
循环体
执行流程:
┌──────────────┐
│ 可迭代对象 │
│ [1,2,3,4,5] │
└──────┬───────┘
│
▼
┌──────────────┐
│ 调用 iter() │ ← 获取迭代器
│ 得到迭代器 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 调用 next() │ ← 每次迭代取一个元素
│ 取出一个元素 │
└──────┬───────┘
│
┌──────▼───────┐
│ 赋值给变量 │ ← 变量 = 取出的元素
└──────┬───────┘
│
▼
┌──────────────┐
│ 执行循环体 │ ← 对这个元素执行操作
└──────┬───────┘
│
▼
┌──────────────┐
│ 还有元素吗? │───是──→ 继续 next()
└──────┬───────┘
│ 否
▼
┌──────────────┐
│ 捕获 StopIteration │
│ 循环结束 │
└──────────────┘
2.2 迭代器协议(核心原理)
Python 中的 for 循环背后是 迭代器协议(Iterator Protocol),这是理解循环的关键:
| 概念 | 说明 | 魔术方法 |
|---|---|---|
| 可迭代对象(Iterable) | 可以迭代的对象(列表、字符串、文件等) | __iter__() |
| 迭代器(Iterator) | 负责逐个返回元素的对象 | __iter__() + __next__() |
| 迭代 | 遍历元素的过程 | — |
手动模拟 for 循环:
# Python 内部 for 循环的等价实现
fruits = ["苹果", "香蕉", "橘子"]
# 1. 获取迭代器
iterator = iter(fruits) # 等价于 fruits.__iter__()
# 2. 不断调用 next() 直到 StopIteration
while True:
try:
fruit = next(iterator) # 等价于 iterator.__next__()
print(f"吃到: {fruit}")
except StopIteration:
break # 没有更多元素了
# 输出:
# 吃到: 苹果
# 吃到: 香蕉
# 吃到: 橘子
2.3 哪些是可迭代对象?
几乎所有 Python 容器都是可迭代的:
# 可迭代对象一览
iterable_list = [
[1, 2, 3], # 列表
(1, 2, 3), # 元组
{1, 2, 3}, # 集合
{"a": 1, "b": 2}, # 字典(默认迭代 key)
"hello", # 字符串
range(5), # range 对象
open("file.txt"), # 文件(按行迭代)
(x for x in range(3)), # 生成器表达式
]
为什么字符串是可迭代的?
for char in "Python":
print(char, end=" ")
# 输出: P y t h o n
字符串本质上是字符的序列,每个字符是一个独立的元素,所以可以迭代。
为什么整数不可迭代?
# for i in 42: # ❌ TypeError: 'int' object is not iterable
# 整数不是容器,不包含元素序列,所以不可迭代
2.4 enumerate() — 同时获取索引和值
当需要 索引 + 元素 时:
fruits = ["苹果", "香蕉", "橘子"]
# 不优雅的方式
for i in range(len(fruits)):
print(i, fruits[i])
# 优雅的方式
for i, fruit in enumerate(fruits):
print(i, fruit)
# 还可以指定起始编号
for i, fruit in enumerate(fruits, start=1):
print(f"第{i}个水果: {fruit}")
2.5 zip() — 并行迭代多个序列
names = ["张三", "李四", "王五"]
scores = [92, 85, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}分")
# 输出:
# 张三: 92分
# 李四: 85分
# 王五: 78分
zip() 的工作原理:
names: "张三" "李四" "王五"
│ │ │
scores: 92 85 78
│ │ │
zip(): ("张三",92) ("李四",85) ("王五",78)
3. range() 函数详解
3.1 range() 的三种调用方式
range(stop) # 0 到 stop-1
range(start, stop) # start 到 stop-1
range(start, stop, step) # start 到 stop-1,步长为 step
3.2 参数详解
| 参数 | 类型 | 必需? | 默认值 | 说明 |
|---|---|---|---|---|
start |
int | 否 | 0 | 起始值(包含) |
stop |
int | 是 | — | 结束值(不包含) |
step |
int | 否 | 1 | 步长(可以为负数) |
3.3 代码示例
# 形式 1: range(stop)
print(list(range(5))) # [0, 1, 2, 3, 4]
# 形式 2: range(start, stop)
print(list(range(2, 7))) # [2, 3, 4, 5, 6]
# 形式 3: range(start, stop, step)
print(list(range(0, 10, 2))) # [0, 2, 4, 6, 8]
print(list(range(10, 0, -2))) # [10, 8, 6, 4, 2]
3.4 为什么 range() 不是列表?
r = range(1000000) # 瞬间完成
l = list(range(1000000)) # 占用大量内存
print(type(r)) # <class 'range'>
print(sys.getsizeof(r)) # 48 字节(无论多大范围)
print(sys.getsizeof(l)) # 超过 8MB
关键原理:惰性求值(Lazy Evaluation)
range(1, 1000000, 2) ← 不生成所有数字,只存储三个值
┌─────────────────────────────────┐
│ range 对象 │
│ │
│ start = 1 (起始值) │
│ stop = 1000000(终止值) │
│ step = 2 (步长) │
│ │
│ 内存占用: 固定 48 字节 │
└─────────────────────────────────┘
│
▼ 迭代时按需计算
1 → 3 → 5 → 7 → ... → 999999
range() 存储的是 计算公式 而不是所有值。只有在迭代时,才按需计算下一个值。这就是为什么它叫 "惰性"(Lazy)。
3.5 反向迭代
# 方式 1: step 为负数
for i in range(5, 0, -1):
print(i, end=" ") # 5 4 3 2 1
print()
# 方式 2: reversed()
for i in reversed(range(1, 6)):
print(i, end=" ") # 5 4 3 2 1
4. break / continue / else 控制流
循环控制语句让循环不再是"一根筋",可以灵活地 提前结束 或 跳过。
4.1 break — 完全退出循环
┌───────────────────┐
│ for x in data: │
└───────┬───────────┘
│
▼
┌───────────────┐
│ 条件满足吗? │──是──→ break ──→ 跳出循环
└───────┬───────┘
│ 否
▼
┌───────────────┐
│ 执行循环体 │
└───────┬───────┘
│
▼
回到循环开始
# 查找第一个负数
numbers = [3, 7, 1, -5, 9, -2, 4]
for num in numbers:
if num < 0:
print(f"找到第一个负数: {num}")
break # 找到就停止,不再继续
print(f"处理: {num}")
# 输出:
# 处理: 3
# 处理: 7
# 处理: 1
# 找到第一个负数: -5
4.2 continue — 跳过当前迭代
┌───────────────────┐
│ for x in data: │
└───────┬───────────┘
│
▼
┌───────────────┐
│ 条件满足吗? │──是──→ continue ──→ 回到循环开始
└───────┬───────┘
│ 否
▼
┌───────────────┐
│ 执行循环体 │
└───────┬───────┘
│
▼
回到循环开始
# 只处理正数,跳过负数
numbers = [3, -1, 7, -5, 9, -2]
for num in numbers:
if num < 0:
continue # 跳过负数
print(f"处理正数: {num}")
# 输出:
# 处理正数: 3
# 处理正数: 7
# 处理正数: 9
4.3 break vs continue vs 普通结束
┌─────────────┐
│ 循环开始 │
└──────┬──────┘
│
▼
┌─────────────────────┐
│ 还有元素吗? / 条件 │──否──→ [else 子句] → 结束
└─────────┬───────────┘
│ 是
▼
┌─────────────────────┐
│ 执行循环体 │
└─────────┬───────────┘
│
┌─────────▼───────────┐
│ │
┌────▼────┐ ┌─────▼─────┐
│ break? │ │ continue? │
└────┬────┘ └─────┬─────┘
│ 是 │ 是
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 跳出循环 │ │ 跳过剩余循环体 │
│ 不执行 else │ │ 回到条件判断 │
└──────────────┘ └──────────────────┘
4.4 for...else — 循环正常结束才执行
这是 Python 独有的特性,初学者容易忽略,但在某些场景下非常有用。
# 场景:判断一个数是否是质数
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
print(f"{n} = {i} × {n//i}")
break
else:
# 只有循环没有被 break 时才会执行
print(f"{n} 是质数!")
return True
return False
is_prime(17) # 17 是质数!
is_prime(15) # 15 = 3 × 5
else 的执行条件:
| 循环结束方式 | else 是否执行? |
|---|---|
正常迭代完毕(无 break) |
✅ 是 |
被 break 中断 |
❌ 否 |
| 从未进入循环(空序列) | ✅ 是 |
| 循环中抛出异常 | ❌ 否 |
# else 在从未进入的循环中也会执行
for x in []:
print("这行不会打印")
else:
print("但 else 会执行!")
# 输出: 但 else 会执行!
4.5 while...else 同理
count = 0
while count < 3:
print(count)
count += 1
else:
print("循环正常结束!")
# 输出: 0 1 2 循环正常结束!
count = 0
while count < 3:
print(count)
count += 1
if count == 2:
break
else:
print("不会执行")
# 输出: 0 1
5. while 循环与无限循环
5.1 基本语法
while 条件表达式:
循环体
执行流程:
┌───────────┐
│ 条件判断 │──否──→ 结束循环
└─────┬─────┘
│ 是
▼
┌───────────┐
│ 执行循环体 │
└─────┬─────┘
│
▼
回到条件判断
5.2 什么时候用 while 而不是 for?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 明确知道迭代次数 | for |
简洁、安全 |
| 不知道多少次,只知道条件 | while |
灵活 |
| 遍历容器元素 | for |
Pythonic |
| 循环可能永远不执行 | for |
空序列=0次 |
| 至少执行一次 | while + 标志位 |
易控制 |
5.3 无限循环(Infinite Loop)
# 这是无限循环——危险!
# while True:
# print("永远停不下来...")
无限循环的两种情况:
- 意外无限循环(Bug)—— 要避免
- 故意无限循环(设计)—— 配合
break使用
意外无限循环示例:
# ❌ 忘记更新计数器
count = 0
while count < 10:
print(count)
# 忘记写 count += 1,count 永远等于 0
# ❌ 条件写错
x = 10
while x > 0: # 应该是 x > 0
print(x)
x += 1 # 应该递减,结果递增了
# ❌ 浮点数精度问题
x = 0
while x != 1.0: # 浮点数精确相等很危险!
x += 0.1
故意无限循环(正确的打开方式):
# ✅ 用户输入验证
while True:
user_input = input("请输入 'quit' 退出: ")
if user_input == 'quit':
break
print(f"你输入了: {user_input}")
5.4 while 实战:猜数字游戏
import random
target = random.randint(1, 100)
attempts = 0
print("🎯 猜数字游戏!范围: 1-100")
while True:
try:
guess = int(input("请输入你的猜测: "))
attempts += 1
if guess < target:
print("📈 太低了!")
elif guess > target:
print("📉 太高了!")
else:
print(f"🎉 恭喜!你猜了 {attempts} 次!")
break
except ValueError:
print("❌ 请输入有效数字!")
6. 循环嵌套
6.1 什么是循环嵌套?
循环嵌套就是 循环里面再套循环。外层循环每执行一次,内层循环执行完整一轮。
外循环 (i = 1) ─→ 内循环 (j = 1, 2, 3)
内循环结束
外循环 (i = 2) ─→ 内循环 (j = 1, 2, 3)
内循环结束
外循环 (i = 3) ─→ 内循环 (j = 1, 2, 3)
内循环结束
6.2 执行次数计算
# 嵌套循环的总执行次数 = 外循环次数 × 内循环次数
for i in range(3): # 执行 3 次
for j in range(4): # 每次执行 4 次
print(f"({i},{j})", end=" ")
print()
# 总共打印了 3 × 4 = 12 个坐标
# (0,0) (0,1) (0,2) (0,3)
# (1,0) (1,1) (1,2) (1,3)
# (2,0) (2,1) (2,2) (2,3)
6.3 用嵌套循环绘制图形
这是理解嵌套循环最好的方式:
# 绘制矩形
for i in range(4):
for j in range(8):
print("*", end="")
print()
# 输出:
# ********
# ********
# ********
# ********
# 绘制直角三角形
for i in range(1, 6):
for j in range(i):
print("*", end="")
print()
# 输出:
# *
# **
# ***
# ****
# *****
7. 实战:九九乘法表
7.1 基础版本
print("=== 九九乘法表 ===")
for i in range(1, 10): # 外层:被乘数 1-9
for j in range(1, i + 1): # 内层:乘数 1-i(不重复)
print(f"{j}×{i}={i*j:2d}", end=" ")
print() # 换行
输出效果:
=== 九九乘法表 ===
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
7.2 理解为什么内层 range 到 i+1
i=1: j 从 1 到 1 → 1×1
i=2: j 从 1 到 2 → 1×2 2×2
i=3: j 从 1 到 3 → 1×3 2×3 3×3
...
j 的范围是 range(1, i+1),保证每行只打印 i 个算式,且只打印下三角部分(j ≤ i),避免重复(如 1×2 和 2×1 是同一个算式)。
7.3 完整实战代码(含多种变体)
详见 code/01-multiplication-table.py
8. 实战:菱形打印
8.1 问题分析
打印一个菱形(高度为 7 行):
*
***
*****
*******
*****
***
*
关键观察:
- 菱形由 空格 和 星号 组成
- 上半部分:空格递减,星号递增
- 下半部分:空格递增,星号递减
- 空格和星号的数量关系:上三角 空格 = n - i, 星号 = 2*i - 1
- 下三角:空格 = i, 星号 = 2*(n-i) - 1
8.2 打印过程分解
行号 空格数 星号数 效果
──────────────────────────
1 3 1 ___*
2 2 3 __***
3 1 5 _*****
4 0 7 *******
5 1 5 _*****
6 2 3 __***
7 3 1 ___*
8.3 代码实现
n = 4 # 上半部分行数
# 上半部分
for i in range(1, n + 1):
# 打印空格
for j in range(n - i):
print(" ", end="")
# 打印星号
for j in range(2 * i - 1):
print("*", end="")
print()
# 下半部分
for i in range(1, n):
# 打印空格
for j in range(i):
print(" ", end="")
# 打印星号
for j in range(2 * (n - i) - 1):
print("*", end="")
print()
详见 code/02-diamond-printing.py
9. 思考题
-
迭代器与索引:为什么 Python 推荐用
for item in list而不是for i in range(len(list))?两种方式的性能差异是什么?当需要修改列表元素时应该怎么做? -
break vs return:在函数中使用循环时,
break和return有什么区别?什么时候用哪个? -
无限循环的哲学:为什么有些程序故意用
while True而不是设定有限条件?在网络服务器、游戏主循环等场景中,这种模式有什么好处? -
for...else 的实际用途:除了判断质数,你能想到
for...else还有什么实际应用场景?例如在搜索、验证等场景中。 -
嵌套循环的优化:下面的两段代码功能相同,为什么
O(n²)版本比O(n²/2)版本执行更多次?
# 版本 A: 1+2+...+n = n(n+1)/2 次
for i in range(n):
for j in range(i, n):
...
# 版本 B: n × n = n² 次
for i in range(n):
for j in range(n):
...