Day05:流程控制:if/for/while------让程序"做决定"和"重复干活"
前两章我们学习了 Python 的基本数据类型和数据结构。今天,我们要给程序装上"大脑"------让它能够根据条件做出判断,能够重复执行任务。这就是流程控制,是编程中最核心的概念之一。
如果说变量和数据类型是程序的"素材",那么流程控制就是程序的"骨架"。没有流程控制,你的代码只能像一条直线一样从头跑到尾,没有任何灵活性。而有了 if、for、while,你的程序就能像真正的 Solver 一样:根据输入的不同走不同的路,根据需要重复计算直到收敛。
本章我们用数学建模中常见的场景来串联所有流程控制语法,让你在学中用、在用中学。
5.1 条件分支:if / elif / else
5.1.1 基本语法
条件分支让你能够根据条件的真假来执行不同的代码块。Python 用 if、elif(即 else if)、else 来实现:
python
x = 5
if x > 0:
print("x 是正数")
elif x == 0:
print("x 是零")
else:
print("x 是负数")
注意 Python 用缩进(通常是 4 个空格)来标记代码块的开始和结束,而不是像其他语言那样用大括号。这是 Python 风格(PEP 8)的核心特征之一,务必保持一致的缩进。
5.1.2 数学建模中的条件判断
场景一:分段函数的实现
在数学建模中,分段函数非常常见。比如惩罚函数、损失函数、或者描述实际物理规律时:
python
def penalty_function(x):
"""
二次惩罚函数:
如果 x >= 0,惩罚为 0
如果 x < 0,惩罚为 x²
"""
if x >= 0:
return 0
else:
return x ** 2
# 测试
for test_x in [-2, -1, 0, 1, 2]:
print(f"x = {test_x:4.1f}, 惩罚 = {penalty_function(test_x):6.2f}")
更复杂的分段函数,比如描述摩擦力的模型:
python
def friction_force(v, mu_s=0.5, mu_k=0.3, normal_force=10.0):
"""
摩擦力模型:
- 静止时:静摩擦力,大小等于外力但不超过最大静摩擦力
- 运动时:动摩擦力,大小恒定
"""
max_static_friction = mu_s * normal_force
if abs(v) < 1e-10: # 近似静止
return 0.0 # 没有相对运动,暂不计算外力
elif v > 0:
return -mu_k * normal_force # 动摩擦力,方向与运动方向相反
else:
return mu_k * normal_force
场景二:约束条件的可行性检查
python
def check_feasibility(x, y, constraints):
"""
检查点 (x, y) 是否满足所有约束条件
constraints: 字典,每个约束有一个 'type'('ineq' 或 'eq')
和一个 'expression' 函数
"""
violations = {}
for name, constraint in constraints.items():
if constraint['type'] == 'ineq':
value = constraint['expression'](x, y)
if value < 0:
violations[name] = f"{name} violated: {value:.4f} < 0"
elif constraint['type'] == 'eq':
value = constraint['expression'](x, y)
if abs(value) > 1e-6:
violations[name] = f"{name} violated: |{value:.6f}| > 1e-6"
return len(violations) == 0, violations
# 定义约束
constraints = {
'g1': {'type': 'ineq', 'expression': lambda x, y: x + y - 10},
'g2': {'type': 'ineq', 'expression': lambda x, y: x - y + 2},
'h1': {'type': 'eq', 'expression': lambda x, y: x**2 + y**2 - 25}
}
# 测试几个点
for point in [(3, 3), (5, 5), (0, 5)]:
feasible, violations = check_feasibility(point[0], point[1], constraints)
status = "可行" if feasible else "不可行"
print(f"点 {point}: {status}")
if violations:
for v in violations.values():
print(f" 违反: {v}")
5.1.3 三元表达式:简洁的条件赋值
当你的条件逻辑只有一行时,Python 提供了简洁的三元表达式语法:
python
# 传统写法
if x >= 0:
sign = 1
else:
sign = -1
# 三元表达式写法(等价)
sign = 1 if x >= 0 else -1
在数学建模中,三元表达式经常用于快速赋值:
python
# 根据误差大小决定输出精度
error = 0.003
precision = 6 if error < 0.01 else 3
print(f"误差 {error:.4f},保留 {precision} 位有效数字")
也可以嵌套使用(但不建议嵌套超过两层,否则可读性下降):
python
# 根据 R² 值评价模型质量
r_squared = 0.87
quality = "优秀" if r_squared >= 0.9 else ("良好" if r_squared >= 0.7 else "需改进")
print(f"R² = {r_squared:.2f},模型评价: {quality}")
5.1.4 多个条件的组合
用 and、or、not 来组合多个条件:
python
temperature = 25.0
humidity = 70.0
# 判断是否适合户外实验
if temperature > 15 and temperature < 35 and humidity < 80:
print("天气适宜,可以进行户外实验")
else:
print("天气条件不满足要求")
在布尔表达式中,Python 会进行短路求值------如果第一个条件已经确定了结果,后面的条件就不会被执行:
python
# 安全地检查列表是否为空再取第一个元素
data = [1, 2, 3]
first_element = data[0] if len(data) > 0 else None
# 下面这个写法更安全:如果 data 为空,后面的 data[0] 根本不会执行
safe_first = data[0] if data else None
5.2 循环:for 语句
5.2.1 基本语法
for 循环用来遍历一个序列(列表、元组、字符串等)或任何可迭代对象:
python
# 遍历列表
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(f"I like {fruit}")
# 遍历数字范围
for i in range(5): # 0, 1, 2, 3, 4
print(i, end=' ')
print()
for i in range(2, 8): # 2, 3, 4, 5, 6, 7
print(i, end=' ')
print()
for i in range(0, 10, 2): # 0, 2, 4, 6, 8(步长为 2)
print(i, end=' ')
print()
range(start, stop, step) 是 Python 中最常用的循环工具,它生成一个等差数列,左闭右开 ------range(5) 生成 0, 1, 2, 3, 4,不包含 5。
5.2.2 enumerate:同时获取索引和值
在很多数学建模场景中,你不仅需要同步遍历多个列表,还需要知道当前是第几个元素:
python
# 传统方式:手动维护索引
measurements = [23.1, 25.6, 27.3, 24.8, 26.1]
for i in range(len(measurements)):
print(f"第 {i+1} 次测量: {measurements[i]}")
# 更优雅的写法:enumerate
for i, value in enumerate(measurements):
print(f"第 {i+1} 次测量: {value}")
# enumerate 还可以指定起始索引
for i, value in enumerate(measurements, start=1):
print(f"第 {i} 次测量: {value}")
5.2.3 zip:并行遍历多个序列
python
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 92, 78]
grades = ['B', 'A', 'C']
# 三个列表同步遍历
for name, score, grade in zip(names, scores, grades):
print(f"{name}: 分数 {score},等级 {grade}")
# 计算每人两次测验的平均分
scores2 = [80, 88, 90] # 第二次测验成绩
averages = [sum(g) / len(g)
for g in zip(scores, scores2)]
zip 在数学建模中特别有用------当你需要同时处理多个相关的数组时,比如自变量和因变量、时间和位移、理论值和观测值等。
5.2.4 遍历字典的几种方式
python
param_dict = {'learning_rate': 0.01, 'epochs': 100, 'batch_size': 32}
# 遍历键
for key in param_dict:
print(f"键: {key}")
# 遍历值
for value in param_dict.values():
print(f"值: {value}")
# 同时遍历键和值(最常用)
for key, value in param_dict.items():
print(f"{key} = {value}")
5.2.5 for 循环在数学建模中的典型应用
应用一:数值积分(黎曼和)
python
def riemann_sum(f, a, b, n=1000):
"""
用黎曼和近似计算定积分 ∫[a,b] f(x)dx
方法:矩形法(左端点)
"""
delta_x = (b - a) / n
total = 0.0
for i in range(n):
x_i = a + i * delta_x
total += f(x_i) * delta_x
return total
# 近似计算 ∫[0,1] x² dx = 1/3
def f(x):
return x ** 2
approximation = riemann_sum(f, 0, 1, n=10000)
exact = 1/3
print(f"黎曼和近似: {approximation:.6f}")
print(f"精确值: {exact:.6f}")
print(f"误差: {abs(approximation - exact):.6f}")
应用二:计算数列的和
python
# 计算调和级数前 1000 项的和
harmonic_sum = 0.0
for i in range(1, 1001):
harmonic_sum += 1.0 / i
print(f"H_1000 = {harmonic_sum:.4f}")
# 计算阶乘
factorial = 1
for i in range(1, 11):
factorial *= i
print(f"10! = {factorial}")
应用三:矩阵乘法
python
def matrix_multiply(A, B):
"""
两个矩阵的乘法(通用实现)
A: m×n 矩阵,B: n×p 矩阵
返回: m×p 矩阵
"""
m = len(A)
n = len(A[0])
p = len(B[0])
# 初始化结果矩阵(全零)
C = [[0.0 for _ in range(p)] for _ in range(m)]
# 三重循环实现矩阵乘法
for i in range(m):
for j in range(p):
for k in range(n):
C[i][j] += A[i][k] * B[k][j]
return C
# 测试
A = [[1, 2, 3],
[4, 5, 6]]
B = [[7, 8],
[9, 10],
[11, 12]]
C = matrix_multiply(A, B)
for row in C:
print(row)
5.3 循环:while 语句
5.3.1 基本语法
while 循环在条件为真时重复执行代码块。与 for 循环不同,while 循环不依赖于一个已知的序列,而是依赖于某个条件的持续真假:
python
# 计算 2 的幂次,直到超过 1000
power = 1
exponent = 0
while power <= 1000:
print(f"2^{exponent} = {power}")
power *= 2
exponent += 1
5.3.2 while 在数值方法中的应用
应用一:牛顿迭代法求根
牛顿迭代法是求解非线性方程 f(x)=0f(x) = 0f(x)=0 最经典的数值方法之一。它的迭代公式为:
xn+1=xn−f(xn)f′(xn)x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}xn+1=xn−f′(xn)f(xn)
python
def newton_method(f, df, x0, tol=1e-10, max_iter=1000):
"""
牛顿迭代法求 f(x) = 0 的根
参数:
f: 目标函数
df: f 的导数
x0: 初始猜测值
tol: 收敛容差
max_iter: 最大迭代次数(防止无限循环)
返回:
近似根,以及迭代历史和是否收敛
"""
x = x0
history = [x]
for i in range(max_iter):
fx = f(x)
dfx = df(x)
# 检查是否已收敛
if abs(fx) < tol:
return x, history, True
# 检查导数是否为零(避免除以零)
if abs(dfx) < 1e-15:
return x, history, False # 导数为零,迭代失败
# 牛顿迭代公式
x_new = x - fx / dfx
# 检查相邻两次迭代的差值
if abs(x_new - x) < tol:
return x_new, history + [x_new], True
x = x_new
history.append(x)
return x, history, False # 达到最大迭代次数仍未收敛
# 用牛顿法求 √2(即求解 x² - 2 = 0)
def g(x):
return x**2 - 2
def g_prime(x):
return 2 * x
root, history, converged = newton_method(g, g_prime, x0=1.5)
print(f"√2 的近似值: {root:.10f}")
print(f"精确值: {2**0.5:.10f}")
print(f"迭代次数: {len(history) - 1}")
print(f"收敛状态: {'✓ 收敛' if converged else '✗ 未收敛'}")
print(f"迭代历史: {history}")
应用二:蒙特卡洛方法估算概率
python
import random
def estimate_probability(target_condition, n_trials=100000):
"""
用蒙特卡洛方法估算满足 target_condition 的概率
"""
success_count = 0
for _ in range(n_trials):
# 在 [0, 1] × [0, 1] 正方形内随机撒点
x = random.random()
y = random.random()
if target_condition(x, y):
success_count += 1
return success_count / n_trials
# 估算点 (x,y) 落在单位圆内的概率(即 π/4)
pi_estimate = 4 * estimate_probability(lambda x, y: x**2 + y**2 <= 1, n_trials=100000)
print(f"π 的蒙特卡洛估计: {pi_estimate:.4f}")
print(f"π 的精确值: {3.14159265:.4f}")
应用三:贪心算法------背包问题
python
def greedy_knapsack(items, capacity):
"""
0-1 背包问题的贪心近似解(按价值密度排序)
items: 列表,每个元素是 (weight, value) 元组
capacity: 背包容量
"""
# 计算价值密度并排序
items_with_ratio = [(i, w, v, v/w) for i, (w, v) in enumerate(items)]
items_with_ratio.sort(key=lambda x: x[3], reverse=True) # 按密度降序
total_value = 0
total_weight = 0
selected = []
for i, w, v, ratio in items_with_ratio:
if total_weight + w <= capacity:
selected.append(i)
total_weight += w
total_value += v
return {
'selected_indices': selected,
'total_weight': total_weight,
'total_value': total_value,
'capacity': capacity,
'utilization': total_weight / capacity * 100
}
# 测试
items = [(2, 3), (3, 4), (4, 5), (5, 8), (2, 4)]
capacity = 5
result = greedy_knapsack(items, capacity)
print(f"选中物品索引: {result['selected_indices']}")
print(f"总重量: {result['total_weight']}/{result['capacity']}")
print(f"总价值: {result['total_value']}")
print(f"容量利用率: {result['utilization']:.1f}%")
5.3.3 for 还是 while?如何选择
这是初学者常问的问题。简单原则:
- 已知迭代次数 (遍历列表、计算前 n 项和等)→ 用
for - 未知迭代次数 (迭代直到收敛、等待某个条件满足等)→ 用
while
在数值算法中,绝大多数迭代方法(牛顿法、梯度下降、模拟退火等)都用 while,因为你无法提前知道需要迭代多少次才能收敛。
5.4 循环控制:break 和 continue
5.4.1 break:提前退出循环
break 立即终止当前所在的循环,跳到循环后面的代码继续执行:
python
# 寻找第一个大于 100 的斐波那契数
a, b = 1, 1
while True:
if a > 100:
print(f"第一个大于 100 的斐波那契数是: {a}")
break
a, b = b, a + b
在数学建模中,break 常用于在找到解之后提前结束搜索:
python
def find_first_prime_above(n):
"""寻找大于 n 的第一个素数"""
candidate = n + 1
while True:
is_prime = True
for divisor in range(2, int(candidate**0.5) + 1):
if candidate % divisor == 0:
is_prime = False
break # 不是素数,跳过当前候选数
if is_prime:
return candidate
candidate += 1
print(f"大于 1000 的第一个素数: {find_first_prime_above(1000)}")
5.4.2 continue:跳过当前迭代
continue 跳过当前这次循环的剩余部分,直接进入下一次迭代:
python
# 打印 1 到 20 之间的所有奇数
for i in range(1, 21):
if i % 2 == 0:
continue # 偶数跳过
print(i, end=' ')
print() # 输出: 1 3 5 7 9 11 13 15 17 19
数学建模实战:数据清洗中的 continue
python
def clean_data(raw_data, missing_threshold=0.5):
"""
数据清洗:跳过缺失值过多的样本
raw_data: 列表的列表,每个内层列表是一个样本的各个特征值
None 表示缺失值
"""
clean_samples = []
for i, sample in enumerate(raw_data):
# 计算缺失比例
missing_count = sum(1 for v in sample if v is None)
missing_ratio = missing_count / len(sample)
if missing_ratio > missing_threshold:
print(f"样本 {i}: 缺失率 {missing_ratio:.1%},跳过")
continue # 缺失太多,丢弃这个样本
clean_samples.append(sample)
print(f"原始样本数: {len(raw_data)}, 清洗后: {len(clean_samples)}")
return clean_samples
# 模拟含缺失值的数据
raw_data = [
[1.0, 2.0, 3.0, 4.0], # 完整
[1.0, None, 3.0, None], # 50% 缺失
[None, None, None, None], # 100% 缺失
[2.0, 3.0, 4.0, 5.0], # 完整
]
cleaned = clean_data(raw_data)
print(f"清洗后的数据: {cleaned}")
5.4.3 else 子句:循环的"善终"
Python 独有的语法------循环可以带 else 子句。它的作用是:当循环正常结束时执行 (即没有被 break 打断):
python
# 检查一个数是否是素数
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 # 找到因子,不是素数
else:
# 循环正常结束(没有找到因子),是素数
return True
print(f"17 是素数: {is_prime(17)}") # True
print(f"15 是素数: {is_prime(15)}") # False
这个 for-else 结构非常符合直觉:
for循环找到了因子 →break→else不执行for循环没找到因子 → 正常结束 →else执行
5.5 嵌套循环
5.5.1 基本概念
循环内部可以再嵌套循环,形成多层迭代。这是处理矩阵、网格搜索、组合问题等的标准工具。
python
# 九九乘法表
for i in range(1, 10):
for j in range(1, i + 1):
print(f"{j}×{i}={i*j:2d}", end='\t')
print()
5.5.2 矩阵运算中的嵌套循环
python
def matrix_transpose(A):
"""矩阵转置"""
m = len(A)
n = len(A[0])
# 转置矩阵 B,B[j][i] = A[i][j]
B = [[A[i][j] for i in range(m)] for j in range(n)]
return B
def matrix_add(A, B):
"""矩阵加法"""
m = len(A)
n = len(A[0])
C = [[A[i][j] + B[i][j] for j in range(n)] for i in range(m)]
return C
# 测试
A = [[1, 2, 3],
[4, 5, 6]]
B = [[7, 8, 9],
[10, 11, 12]]
print("A 的转置:")
for row in matrix_transpose(A):
print(row)
print("\nA + B:")
for row in matrix_add(A, B):
print(row)
5.5.3 嵌套循环的效率意识
嵌套循环虽然直观,但在处理大规模数据时可能非常慢。Python 的循环本身就有开销,双重循环的时间复杂度是 O(n2)O(n^2)O(n2),三重循环是 O(n3)O(n^3)O(n3)。在竞赛中,如果数据量很大,要注意:
python
# ❌ 低效:双重循环逐项求和
total = 0
for i in range(10000):
for j in range(10000):
total += i * j
# ✅ 高效:利用数学公式 O(1) 计算
n = 10000
total_formula = (n * (n - 1) // 2) ** 2 # (1+2+...+n)²
print(f"循环计算: {total}")
print(f"公式计算: {total_formula}")
print(f"结果一致: {total == total_formula}")
在数学建模中,能用公式就不用循环,能用向量化操作就不用嵌套循环。这是我们后面几章(NumPy 和 SciPy)的重要内容。
5.6 综合实战:一个完整的迭代优化流程
把 if、for、while、break、continue 串在一起,看一个完整的例子:
python
def gradient_descent(f, df, x0, lr=0.01, tol=1e-8, max_iter=10000):
"""
梯度下降法求解无约束优化问题 min f(x)
参数:
f: 目标函数
df: f 的梯度(导数)
x0: 初始点
lr: 学习率
tol: 收敛容差
max_iter: 最大迭代次数
返回:
最优解、最优值、迭代历史、是否收敛
"""
x = x0
history = [{'iter': 0, 'x': x, 'f(x)': f(x)}]
for iteration in range(1, max_iter + 1):
gradient = df(x)
# 检查梯度是否已经足够小(可能已到达极值点)
if abs(gradient) < tol:
history[-1]['converged'] = True
return x, f(x), history, True
# 更新参数
x_new = x - lr * gradient
f_new = f(x_new)
# 记录历史
entry = {'iter': iteration, 'x': x_new, 'f(x)': f_new}
# 如果函数值不下降,说明学习率太大,减小学习率
if f_new > history[-1]['f(x)']:
lr *= 0.5 # 学习率减半
entry['lr'] = lr
continue # 跳过本次记录的完整性,重新计算
history.append(entry)
# 检查是否收敛
if abs(f_new - history[-2]['f(x)']) < tol:
entry['converged'] = True
return x_new, f_new, history, True
# 达到最大迭代次数
history[-1]['converged'] = False
return x, f(x), history, False
# 测试:最小化 f(x) = x²
def f(x):
return x**2
def df(x):
return 2 * x
result_x, result_f, history, converged = gradient_descent(
f, df, x0=10.0, lr=0.1, tol=1e-8
)
print(f"最优解 x* = {result_x:.10f}")
print(f"最优值 f(x*) = {result_f:.10f}")
print(f"收敛状态: {'✓ 收敛' if converged else '✗ 未收敛'}")
print(f"迭代次数: {len(history) - 1}")
print(f"前 5 次迭代:")
for entry in history[:5]:
print(f" iter={entry['iter']:3d}, x={entry['x']:.6f}, f(x)={entry['f(x)']:.6f}")
这个例子把本章学到的所有流程控制结构都用上了:
for循环控制迭代次数if判断收敛条件和学习率调整continue在函数值不下降时跳过本次记录max_iter作为安全网防止无限循环
5.7 常见陷阱与最佳实践
5.7.1 陷阱一:无限循环
python
# ❌ 危险!这个循环永远不会结束
i = 0
while i < 10:
print(i)
# 忘记写 i += 1 了!
# ✅ 正确做法:总是确保循环条件最终会变为假
i = 0
while i < 10:
print(i)
i += 1
在数学建模中,数值迭代的无限循环尤其危险------比如牛顿法可能在某些情况下不收敛。务必设置最大迭代次数作为保险。
5.7.2 陷阱二:在循环中修改正在遍历的列表
python
numbers = [1, 2, 3, 4, 5]
# ❌ 危险!遍历时删除元素会导致索引错位
for i in range(len(numbers)):
if numbers[i] % 2 == 0:
numbers.pop(i) # 删除后列表变短,后面的元素索引前移
# ✅ 正确做法:创建新列表
evens_only = [x for x in numbers if x % 2 == 0]
# 或者从后往前遍历删除
for i in range(len(numbers) - 1, -1, -1):
if numbers[i] % 2 == 0:
numbers.pop(i)
5.7.3 陷阱三:浮点数作为循环边界
python
# ❌ 不推荐:浮点数精度可能导致循环次数不符合预期
x = 0.0
while x != 1.0:
x += 0.1
print(x, end=' ')
# 由于浮点精度,这个循环可能永远不停!
# ✅ 正确做法:用整数计数
n_steps = 10
for i in range(n_steps):
x = i * 0.1
print(x, end=' ')
print()
5.7.4 最佳实践
-
给 while 循环设置上限:任何 while 循环都应该有明确的退出条件,最好加上最大迭代次数。
-
优先使用 for 而非 while :如果你知道迭代次数,用
for比while更安全、更简洁。 -
避免在循环体内做昂贵的计算:把可以提前算好的东西放到循环外面:
python
# ❌ 低效:每次循环都重新计算 len(data)
for i in range(len(data)):
# ...
# ✅ 高效:len(data) 只计算一次
n = len(data)
for i in range(n):
# ...
-
用
enumerate替代手动索引:更清晰,也更不容易出错。 -
善用
else子句 :for-else和while-else是 Python 的特色语法,用好了能让代码更优雅。
本章小结
本章我们系统学习了 Python 的流程控制结构:
if / elif / else:条件分支,让程序根据不同情况走不同路径。掌握分段函数实现、约束检查、三元表达式等典型用法。for循环 :遍历序列,配合range()、enumerate()、zip()可以应对绝大多数迭代需求。while循环:在条件为真时重复执行,适合迭代算法和未知次数的情形。break和continue:精确控制循环的退出和跳过。- 嵌套循环:处理矩阵、网格等多维数据结构的基本工具。
- 循环的
else子句:Python 独有的语法,在循环正常结束时执行。
通过牛顿迭代法、梯度下降、数据清洗等数学建模场景的综合实战,我们看到了流程控制如何把这些零散的语法单元组合成解决实际问题的工作流。
关键要点回顾:
range(start, stop, step)生成等差数列,左闭右开enumerate()同时获取索引和值,zip()并行遍历多个序列- 给
while循环设上限,避免无限循环 - 浮点数不适合作为循环边界条件
- 能用公式就不用循环,能用向量化就不用嵌套循环
下一步
今天学了流程控制,明天我们将进入本章的最后一节------函数 。学会了 if、for、while 之后,你可能会发现很多代码逻辑反复出现。函数就是解决这个问题的:把一段代码打包成一个"黑盒子",定义一次,随时调用。
有了函数,你的代码会从"脚本"升级为"程序",可读性和复用性都会大幅提升。别忘了把今天学的迭代法用函数封装起来,明天我们会看到如何用函数让数学建模的代码更加整洁和专业。
继续前进!