第 3 章 流程控制¶

本章主要内容如下:

  • 3.1 条件语句
  • 3.2 循环与遍历
  • 3.3 异常处理语句
  • 3.4 本章小结与自测

3.0 课前导入与学习路线¶

在真实的财务/会计场景里,我们几乎每天都要做两类事情:

  1. 分情况处理:例如不同逾期天数采用不同的坏账计提规则
  2. 批量处理:例如对一整张交易明细表逐行汇总、筛选、检查

这些任务背后对应的就是本章三件套:

  • 条件语句:让程序做选择
  • 循环与遍历:让程序批量执行
  • 异常处理:让程序在遇到脏数据时仍然可控

本章学习目标¶

完成本章后,你应当能够:

  • 读懂并写出常见的 if / elif / else 结构,并能用布尔表达式组合条件
  • 用 for 遍历列表、元组、字符串、字典,并用 range / enumerate / zip 提升代码可读性
  • 理解 while 的适用场景:直到……为止,并能用 break / continue 控制循环
  • 用 try / except 处理常见报错,例如 ValueError、ZeroDivisionError,让程序更稳健

3.1 条件语句(Conditional Statements)¶

3.1.1 比较运算符和逻辑运算符¶

比较运算符:

  • 相等/不等:==、!=
    • 注意:= 是赋值运算符,不要和 == 混淆
  • 大小比较:>、>=、<、<=
  • 成员关系:in / not in(是否属于某个集合/列表/字符串)

逻辑运算符:

  • 逻辑运算:and、or、not

比较运算符和逻辑运算符的结果是布尔值(True 或 False),它们可以组合成更复杂的条件表达式。

运算优先级:算术运算符 > 比较运算符 > 逻辑运算符 (not > and > or) > 赋值运算符

下面用一个“逾期天数与风险标签”的例子快速感受一下。

In [ ]:
# 示例:应收账款(Accounts Receivable)逾期天数与风险标签
days_past_due = 75  # 逾期天数

is_overdue = days_past_due > 0 # 请注意,这里的比较运算符是 `>`,而不是赋值运算符 `=`
is_high_risk = days_past_due >= 90
is_medium_risk = (days_past_due >= 60) and (days_past_due < 90)
is_medium_risk = (90 > days_past_due >= 60)  # 也可以这样写,效果一样

print("is_overdue:", is_overdue)
print("is_high_risk:", is_high_risk)
print("is_medium_risk:", is_medium_risk)

3.1.2 “真值”规则(Truthiness)¶

在 if 条件: 里,条件不一定要写成 x > 0 这种比较式。很多对象会被自动当成“真/假”:

  • 会被当成 False:0、0.0、""(空字符串)、[](空列表)、{}(空字典)、None
  • 其他多数情况会被当成 True

这在“有没有数据”“列表是否为空”等场景非常常用。

In [ ]:
# 示例:用“真值规则”判断是否有数据
transactions = []   # 空列表 => False
if transactions:
    print("有交易数据,需要汇总。")
else:
    print("没有交易数据,跳过汇总。")

note = ""  # 空字符串 => False
if not note:
    print("备注为空。")

3.1.3 if / elif / else 结构¶

基本语法:

if 条件1:
    代码块1
elif 条件2:
    代码块2
else:
    代码块3

注意两点:

  1. 条件语句后面以冒号 : 结尾
  2. 代码块要缩进(通常 4 个空格)

下面用“应收账款逾期分层”做一个完整的 if-elif-else 例子。

In [ ]:
# 示例:应收账款逾期分层(aging bucket)
days_past_due = 120

if days_past_due <= 0:
    bucket = "未逾期"
elif days_past_due <= 30:
    bucket = "1-30天"
elif days_past_due <= 60:
    bucket = "31-60天"
elif days_past_due <= 90:
    bucket = "61-90天"
else:
    bucket = "90天以上"

print("逾期天数:", days_past_due, "=> 分层:", bucket)


# if语句可以没有else部分
score = 85
if score >= 60:
    print("及格")

# if-elif语句也可以没有else部分
score = 85
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
print("分数:", score, "=> 等级:", grade)

3.1.4 嵌套判断(Nested if)¶

当“先判断大类,再判断子类”时,嵌套 if 很自然,例如:

  • 先判断是否逾期

  • 再判断是否需要计提(例如金额很小可忽略)

  • 通过缩进来区分层级关系,注意不要混淆不同层级的条件语句。

In [ ]:
# 示例:先判断是否逾期,再判断金额是否需要计提
days_past_due = 35
amount = 180  # 金额(元)

if days_past_due > 0:
    # 已逾期
    if amount >= 200:
        action = "需要关注并计提/催收"
    else:
        action = "已逾期,但金额较小,记录即可"
else:
    action = "未逾期"

print(action)

3.1.5 条件表达式(Conditional Expression)¶

有些“非常短的二选一”可以写成一行,也称为三元表达式(ternary expression):

结果 = A if 条件 else B

适合用在简单赋值上,不建议滥用;表达式过长会显著降低可读性。

In [ ]:
# 示例:根据利润正负给出标签
profit = -12.5

# if profit >=0:
#     label = '盈利'
# else:
#     label = '亏损'
# print(profit, label)
# 以上四行代码与以下两行代码起到完全一模一样的效果

label = "盈利" if profit >= 0 else "亏损"
print(profit, label)

练习 3-1:费用报销规则判断¶

假设一笔报销包含两个字段:

  • category(类别):"travel"、"meal"、"other"
  • amount(金额,元)

规则:

  • travel 且金额 > 3000:标记为“需要主管审批”
  • meal 且金额 > 500:标记为“需要说明”
  • 其他情况:标记为“正常”

请补全下面代码,让它输出正确的 flag。

In [5]:
category = 'travel'
amount = 3200

if category == 'travel' and amount > 3000:
    print('需要主管审批')
elif category == 'meal' and amount >500:
    print('需要说明')
else:
    print('正常')
需要主管审批

双击阅读参考答案


3.2 循环与遍历(Loops & Iteration)¶

本节先讲 for(更常用、遍历更自然),再讲 while(“直到……为止”的停止条件更灵活)。

在会计/财务的数据处理里,循环最常见的任务是:

  • 对一批明细逐笔汇总(例如交易金额、费用报销)
  • 对一张表逐行检查与标记(例如异常值、缺失值)
  • 生成按期序列(例如按月折旧、分期还款表)

3.2.1 可迭代对象(Iterable)是什么?¶

能被 for ... in ... 逐个取出元素的对象,称为可迭代对象(iterable)。常见的有:

  • 列表 list、元组 tuple、字符串 str、集合 set
    • 字符串作为可迭代对象时,每次迭代会得到一个字符(长度为1的字符串)
  • 字典 dict(默认遍历 key;也可以遍历 values()、items())

你只需要记住一句话:只要能“一个一个拿出来处理”,就可以用 for。

3.2.2 常见容器的遍历¶

这些内容原来分散在第二章的数据结构部分,但它们真正的使用场景是在 for 循环里做批量处理。

先把最常见的三类遍历方式放在一起:列表、元组、字典。

3.2.2.1 遍历列表¶

列表是可迭代对象,可用 for 逐个访问元素。

In [ ]:
# 遍历列表:逐个访问元素
amounts = [120.0, 88.5, 230.0]  # 一组金额

for x in amounts:
    print(x)
In [9]:
amounts = [120.0, 88.5, 230.0]  # 一组金额
x = amounts[0]
print(x)
x = amounts[1]
print(x)
x = amounts[2]
print(x)
print('循环结束。')

# 但是这种方法在列表更新后无法自动更新遍历内容。相比之下,for循环的功能更加强大,可以随列表的变化进行实时更新。
120.0
88.5
230.0
循环结束。
In [14]:
amounts = [120.0, 88.5, 230.0]  # 一组金额

for x in amounts:
    x += 1
    print(x)
121.0
89.5
231.0

3.2.2.2 遍历元组¶

元组也是可迭代对象,可以用 for 逐个访问其中的元素。
(因为元组不可修改,所以它常用来表示“结构固定的一条记录”。)

In [ ]:
# 遍历元组:逐个访问元素
t = ("Sales", "COGS", "Rent")

for x in t:
    print(x)

3.2.2.3 遍历字典 dict:keys / values / items¶

字典也是可迭代对象。默认遍历的是“键”(keys)。常见三种遍历方式:

  • for k in d: 或 for k in d.keys():遍历键
  • for v in d.values():遍历值
  • for k, v in d.items():同时遍历键和值(最常用)
In [ ]:
# 示例字典
d = {"Sales": 1700.50, "COGS": 630.20, "Rent": 15000.00}

# 1) 遍历键(默认就是键)
for k in d:
    print("key =", k)

print("---")

# 也可以写得更明确:遍历键
for k in d.keys():
    print("key =", k)

print("---")

# 2) 遍历值
for v in d.values():
    print("value =", v)

print("---")

# 3) 同时遍历键和值(最常用)
for k, v in d.items():
    print(k, "->", v)

3.2.2.4 遍历集合(了解)¶

集合是可迭代对象,但元素顺序不保证一致。若需要稳定顺序,可先 sorted() 再遍历。

In [ ]:
# 遍历集合(了解):集合是无序的,遍历顺序不保证一致
s = {"b", "a", "c"}

for x in s:
    print(x)

print("sorted iteration:")
for x in sorted(s):   # sorted(...) 返回排好序的列表,顺序稳定
    print(x)

3.2.3 for 循环¶

基础语法:

for 变量 in 可迭代对象:
    循环体(重复执行)

for 的优势是:不容易写出死循环,适合“对一批数据逐个处理”。

这里的 变量是一个临时变量,可以用任意变量名。 依然需要注意冒号和缩进。

In [ ]:
# 示例 1:批量汇总交易金额(收入为正,支出为负)
transactions = [100, -20, 300, -50]  
total = 0

for x in transactions:
    total += x  # 逐笔累加

print("总额 total =", total)

例题:把交易分成“流入/流出”(构建新列表)¶

目标:把 transactions 拆成两份列表:

  • inflows:收入(金额 ≥ 0)
  • outflows:支出(金额 < 0)
In [ ]:
transactions = [120, -30, 60, 80, -10, -50, 200, -40]

inflows = []
outflows = []

for amt in transactions:
    if amt >= 0:
        inflows.append(amt)
    else:
        outflows.append(amt)

print("inflows =", inflows)
print("outflows =", outflows)
print("净额 net =", sum(inflows) + sum(outflows))

例题:生成“累计余额曲线”(running balance)¶

很多财务图表/审计检查需要“逐笔累计”的余额序列。

给定交易列表 tx,生成一个新列表 balance_curve:

  • balance_curve[i] 表示处理到第 i+1 笔交易后的累计余额
In [ ]:
tx = [1000, -200, -150, 500, -120]
balance = 0
balance_curve = []

for amt in tx:
    balance += amt
    balance_curve.append(balance)

print("balance_curve =", balance_curve)

3.2.3.1 range:生成整数序列(常用于“固定次数”)¶

range 常见三种写法:

  • range(stop):0 到 stop-1 (左闭右开)
  • range(start, stop):start 到 stop-1
  • range(start, stop, step):步长为 step

注意: range 生成的序列从0开始,默认步长为1。

典型用途:12 个月、5 次迭代、从第 1 行到第 N 行。

In [ ]:
list(range(5)), list(range(2, 8)), list(range(1, 10, 2))

示例:计算 1 到 100 的自然数总和(for + range)¶

这是最经典的“循环累加”模板之一。

In [ ]:
total = 0
for i in range(1, 101): 
    total += i
print("1~100 总和 =", total)

3.2.3.2 enumerate:同时拿到索引和元素¶

当你既需要元素,也需要它的位置(索引 index)时,用 enumerate 更清晰:

for idx, value in enumerate(iterable, start=1):
    ...

下面用“逐笔打印交易编号”举例。

transactions = [100, -20, 300, -50]

for idx, amt in enumerate(transactions, start=1):
    print(f"第{idx}笔金额 = {amt}")

--->

3.2.3.3 zip:并行遍历多个序列当两列数据需要“一一对应”处理时,用 zip 很方便(例如:科目名和金额一一对应)。¶
accounts = ["Cash", "A/R", "Inventory"]
amounts  = [5000, 12000, 8000]

for acc, amt in zip(accounts, amounts):
    print(acc, "=>", amt)

--->

例题:部门报销汇总(用 dict 累加)¶

输入:报销明细(部门、金额)
输出:每个部门的汇总金额(部门→合计)

提示:dict.get(key, default) 可以让“第一次出现的部门”更好处理。

In [ ]:
records = [
    {"dept": "Sales", "amount": 1200},
    {"dept": "Sales", "amount": 800},
    {"dept": "R&D",  "amount": 2000},
    {"dept": "HR",   "amount": 500},
    {"dept": "R&D",  "amount": 700},
]

dept_sum = {}

for r in records:
    dept = r["dept"]
    amt = r["amount"]
    dept_sum[dept] = dept_sum.get(dept, 0) + amt

print("dept_sum =", dept_sum)

3.2.3.4 break / continue / for-else:控制循环¶
  • break:立刻退出循环,需注意若有循环嵌套,break 只会退出当前所在的那一层循环
  • continue:跳过本轮,进入下一轮
  • for-else:如果循环“没有被 break 打断”,就执行 else

for-else 常用于“查找”:找到就 break;没找到就走 else。

In [ ]:
# 示例:在列表中“找到第一个奇数”就停止(break)
nums = [2, 4, 6, 9, 10, 17]      # 一组数字

for x in nums:               # 逐个遍历 nums
    if x % 2 == 1:           # 判断 x 是否为奇数(除以 2 余 1)
        print("找到奇数:", x)  # 找到后输出
        break                # 立刻退出循环(不再继续往后找)
In [ ]:
# 示例:跳过缺失值,汇总有效金额
amounts = [100, "N/A", 200, None, 50]

total = 0
for x in amounts:
    if (x is None) or (x == "N/A"):
        continue
    total += x

print("total =", total)
In [ ]:
# 示例:for-else(查找是否存在负数余额)
balances = [120, 80, 50, 0, 30]

for b in balances:
    if b < 0:
        print("发现负数余额:", b)
        break
else:
    print("未发现负数余额")

for 的执行顺序¶
  1. 从可迭代对象中依次取出一个元素,赋值给循环变量
  2. 执行一次循环体
  3. 回到第 1 步,直到没有元素可取为止

练习 3-2:用 for 计算 12 个月累计现金流¶

已知每个月的现金流(正数为流入,负数为流出),请用 for 计算:

  1. 全年累计现金流 total_cf
  2. 现金流为正的月份个数 n_positive

要求:只用 for + if(不使用 sum() 直接求和)。

In [ ]:
# 假设已有 12 个月现金流列表

cash_flows = [1200, -300, 500, -800, 0, 600, -200, 100, -50, 900, -400, 700]



total_cf = 0        # 全年累计现金流

n_positive = 0      # 现金流为正的月份个数

双击阅读参考答案


3.2.4 while 循环¶

基础语法:

while 条件:
    循环体

while 更适合这类任务:

  • 不知道要循环多少次
  • 只知道“什么时候停止”(例如余额不足、达到阈值、误差足够小)

例题:现金余额逐月扣减,直到不足以支付(while)¶
In [ ]:
cash = 1000 # 初始现金余额
monthly_payment = 230 # 每月支付金额
month = 0 # 定义初始值,记录循环的次数

while cash >= monthly_payment:
    cash -= monthly_payment
    month += 1 # 每次循环都要记得更新 month 的值,否则会死循环

print("可支付月数 =", month)
print("最后余额 =", cash)

例题:按月计提折旧,直到达到残值(while)¶

直线法折旧(straight-line depreciation)常见的循环思路是:

  • 每月把账面价值(book value)减少一笔折旧
  • 当继续计提会低于残值(salvage value)时停止
In [ ]:
cost = 120000
salvage = 20000
life_months = 60

monthly_dep = (cost - salvage) / life_months
book_value = cost
m = 0

while book_value - monthly_dep >= salvage:
    book_value -= monthly_dep
    m += 1

print("已计提月数 =", m)
print("期末账面价值 =", round(book_value, 2))

例题:贷款余额逐月计息并还款(while + 最大迭代次数)¶

现实里可能出现“还款额太小,余额越还越多”的情况。
为了避免写出停不下来的 while,建议加一个最大月数 max_months。

In [ ]:
balance = 10000
monthly_rate = 0.01   # 月利率 1%
payment = 300         # 每月固定还款
month = 0
max_months = 600

while (balance > 0) and (month < max_months):
    interest = balance * monthly_rate
    balance = balance + interest - payment
    month += 1
    if balance <= 0:
        balance = 0
        break

print("已还月数 =", month)
print("剩余余额 =", round(balance, 2))
if month == max_months:
    print("提示:达到 max_months,说明还款额可能过小或需要调整参数。")

while + break:输入控制(模板)¶

课堂上常见的“输入 q 结束”模式如下。
说明:在 notebook 里直接运行 input() 会等待输入;下面给出模板,理解思路即可。

In [ ]:
while True:
    s = input("请输入金额,输入 q 退出:")
    if s == "q":
        break
    amt = float(s)
    print("你输入的金额是:", amt)

while 的执行顺序¶
  1. 先判断条件是否为真(True)
  2. 条件为真:执行循环体一次,然后回到第 1 步
  3. 条件为假:退出循环

写 while 时要同时想清楚两件事:

  • 什么时候继续(条件)
  • 什么时候一定会停(让条件变为 False,或用 break)

练习 3-3:用 while 模拟“库存清零”¶

仓库期初库存 stock,每周销量序列 sales(按顺序发生)。
请用 while 计算:库存最多能撑多少周(week)。

规则:

  • 只要 stock > 0 就继续循环
  • 每循环一次:库存减少当周销量,周数加 1
  • 若销量超过库存,则库存归零并停止(不出现负库存)
In [ ]:
stock = 120

sales = [15, 20, 10, 25, 30, 40]   # 每周销量(不固定)

双击阅读参考答案

3.2.5 for vs while:怎么选?¶

经验法则:

  • 你有“一批数据”(列表/字典/表格行)要逐条处理 → 优先用 for
  • 你只知道“什么时候停”(余额不足、达到阈值、输入有效) → 更可能用 while

写 while 时要特别留意:循环体里是否会改变条件相关变量。
在批量处理中,如果你需要“无论成功/失败都要记录进度或做清理”,可以配合 try/finally,见 3.3.4。

3.3 异常处理语句(Exception Handling)¶

现实数据常常“不完美”:

  • 金额字段里混入了 "N/A"、空字符串、None
  • 分母为 0(比如做比例时)
  • 字典缺少某个键(字段缺失)

如果不做处理,程序可能直接报错并停止。异常处理的目标不是“隐藏问题”,而是:

  • 让程序在可控范围内继续运行
  • 把问题记录下来,便于后续清洗/回溯

3.3.1 最常见的几类错误¶

  • ValueError:类型转换失败(例如 int("abc"))
  • ZeroDivisionError:除以 0
  • KeyError:字典没有这个键
  • TypeError:类型不匹配(例如把字符串和数字直接相加)

下面用 try / except 来“兜底”。

In [ ]:
# 示例:把字符串金额转成 float,并捕获 ValueError
raw_amount = "12.5a"

try:
    amt = float(raw_amount)
    print("转换成功:", amt)
except ValueError:
    print("转换失败:raw_amount =", raw_amount)

3.3.2 批量处理中最常见的写法:循环 + try/except¶

思路:

  • 对每条记录尝试转换
  • 成功就纳入计算
  • 失败就记录下来(例如放到 invalid 列表)
In [ ]:
raw_amounts = ["100", "20.5", "N/A", "", None, "30"]
valid = []
invalid = []

for x in raw_amounts:
    try:
        amt = float(x)
        valid.append(amt)
    except (ValueError, TypeError):
        invalid.append(x)

print("valid:", valid)
print("invalid:", invalid)
print("sum(valid) =", sum(valid))

3.3.3 捕获“具体异常” vs 捕获“所有异常”¶

课堂建议:

  • 尽量捕获你能预期到的异常类型(例如 ValueError、TypeError)
  • 不要随手写 except Exception: 然后什么都不做;这会把真正的程序错误也“吞掉”,后续很难排查

下面给出一个“安全除法”的例子:利润率 = profit / revenue。

In [ ]:
def safe_margin(profit, revenue):
    """安全计算利润率:遇到分母为0则返回 None"""
    try:
        return profit / revenue
    except ZeroDivisionError:
        return None

print(safe_margin(10, 100))
print(safe_margin(10, 0))

3.3.4 try/except/else/finally¶

  • else:当 try 块没有报错时才执行(用于把“成功路径”写得更清楚)
  • finally:无论是否报错都会执行(常用于资源释放与收尾工作,例如关闭文件、写进度、回滚临时状态)

先用一个很短的示例熟悉结构。

In [ ]:
raw = "88"

try:
    x = int(raw)
except ValueError:
    print("raw 不是整数")
else:
    print("转换成功,x =", x)
finally:
    print("finally:这句一定会执行")

finally 在循环中的常见用法:每轮处理都做“收尾动作”¶

批量处理时常见的需求是:

  • 本轮成功/失败都要记录进度(否则中途报错就不知道处理到哪)
  • 本轮结束都要做一次清理(例如重置临时变量)

下面的例子演示:即使遇到异常值或触发 break,finally 也会执行。

In [ ]:
items = [10, 5, 0, 2]

for x in items:
    try:
        print("处理 x =", x)
        if x == 0:
            print("遇到关键值:停止处理")
            break
        y = 100 / x
        print("结果 y =", y)
    finally:
        # 真实业务里这里可能是:写进度日志、关闭资源、提交/回滚等
        print("finally:完成本轮收尾")

练习 3-4:批量计算“单笔税额”,并记录问题行¶

你有一批报销记录,每条记录包含:

  • amount(金额,字符串或数字)
  • tax_rate(税率,例如 0.06)

要求:

  • 计算每条记录的 tax = amount * tax_rate
  • 遇到无法转换的金额(例如 "N/A"、空值)时,不报错,改为把该条记录加入 bad_records
  • 输出 tax_list(每条记录的税额,坏记录可跳过)

双击阅读参考答案

3.4 本章小结与自测¶

小结(你现在应该会)¶

  • 会写 if / elif / else,并能用 and / or / not 组合条件
  • 会用 for 遍历列表/字典,能用 range / enumerate / zip
  • 理解 while 的适用场景,并能用 break / continue 控制循环
  • 会用 try / except 处理常见异常,让批量处理更稳健

快速自测¶

  1. if []: 会进入 if 分支吗?为什么?
  2. break 和 continue 的区别是什么?
  3. 为什么不建议无脑写 except Exception: pass?
  4. for k, v in d.items(): 中,items() 返回的是什么结构? --->