# 教案：第08节 —— 异常进阶

---

## 元信息

| 项目 | 内容 |
|------|------|
| **课程编号** | G4_第08节 |
| **GESP等级** | **四级**（GESP Python 四级考点） |
| **适用年龄** | 11-14 岁（编程高手阶段） |
| **核心知识点** | 多 except 捕获、else 分支、finally 收尾、raise 主动抛出异常 |
| **记忆钩子** | finally = 不管怎样都要做的事 raise = 自己扔炸弹 except = 接住炸弹 |
| **身体锚点** | ① try（抛球） ② except（接球） ③ else（庆祝） ④ finally（鞠躬） |

---

## 课程目标

1. **掌握** 多 except 捕获不同类型的异常
2. **理解** else 分支的执行时机（无异常时执行）
3. **掌握** finally 的必执行特性（收尾操作）
4. **运用** raise 主动抛出异常（自定义错误）

---

## 五段式 60 分钟教学流程

---

### 第一段：导入（00:00 - 10:00）—— 异常处理三兄弟

#### 教师活动

| 时间 | 活动内容 | 教学方式 |
|------|---------|---------|
| 0-3min | 提问："如果用户输入 abc，int() 会怎样？" 演示程序崩溃 | 问题引入 |
| 3-6min | "Python 提供了'异常处理三兄弟'——try、except、finally" | 概念引入 |
| 6-10min | 展示今天要学的完整结构：try-except-else-finally 一条龙 | 目标宣布 |

#### 导入代码

```python
# 基础异常处理
try:
    n = int(input("请输入一个数字: "))
    print(f"你输入了: {n}")
except ValueError:
    print("输入错误！请输入数字！")
finally:
    print("程序结束。")

# 今天我扩展成完整版
try:
    n = int(input("请输入被除数: "))
    m = int(input("请输入除数: "))
    result = n / m
except ValueError:
    print("请输入有效数字！")
except ZeroDivisionError:
    print("除数不能为 0！")
else:
    print(f"计算成功: {n} / {m} = {result}")
finally:
    print("运算结束，收尾工作已完成。")
```

#### 生活类比

> **异常处理 = 马戏团表演**
> try：杂技演员抛球（可能出错的代码）
> except：助手接住掉落的球（捕获异常）
> else：观众鼓掌（没出错就庆祝）
> finally：演员鞠躬谢幕（不管怎样都要做）

#### 身体锚点

> **【身体锚点 — 异常处理四步】**
> try：双手向上做抛球动作
> except：双手向下做接球动作
> else：双手高举做胜利动作
> finally：弯腰做鞠躬谢幕动作

---

### 第二段：知识点精讲（10:00 - 30:00）

#### 2.1 多 except 捕获（7min）

**为什么需要多 except？**

```python
# 一段代码可能抛出多种不同类型的异常
# 每种异常需要不同的处理方式

try:
    n = int(input("请输入被除数: "))
    m = int(input("请输入除数: "))
    result = n / m
    print(f"结果: {result}")
except ValueError:
    # 输入不是数字时触发
    print("错误：请输入有效的整数！")
except ZeroDivisionError:
    # 除数为 0 时触发
    print("错误：除数不能为 0！")
except Exception as e:
    # 其他未知异常（兜底）
    print(f"未知错误: {e}")
```

**多 except 的执行规则：**

| 规则 | 说明 | 示例 |
|------|------|------|
| 从上到下匹配 | 先匹配前面的 except | ValueError 比 Exception 靠前 |
| 只执行第一个匹配的 | 不会同时进入多个 | 触发了 ValueError 就不进 ZeroDivisionError |
| 最后可以加 Exception | 兜底捕获所有剩余异常 | except Exception as e |
| as 关键字 | 获取异常对象 | except ValueError as e |

**异常类型层级：**

```
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- Exception
      +-- ValueError
      +-- ZeroDivisionError
      +-- TypeError
      +-- FileNotFoundError
      +-- ...（其他所有内置异常）
```

注意：捕获 Exception 就能捕获到它所有的子类异常。

#### 2.2 else 分支（5min）

```python
try:
    n = int(input("请输入数字: "))
    result = 100 / n
except ValueError:
    print("输入不是数字！")
except ZeroDivisionError:
    print("不能除以 0！")
else:
    # 只有 try 块没有抛出任何异常时才会执行
    print(f"计算成功！100 / {n} = {result:.2f}")
    print("（else 块：没有异常，执行庆祝代码）")
finally:
    print("（finally 块：不管怎样都执行）")
```

**else 的作用：**

| 场景 | else 执行？ |
|------|-----------|
| try 正常执行完毕 | 是 |
| try 抛出异常被 except 捕获 | 否 |
| try 抛出异常未被捕获 | 否（程序崩溃） |

**为什么要用 else 而不是把代码放 try 里？**

```python
# 不好的写法：把所有代码放 try 里
try:
    n = int(input("请输入数字: "))
    result = 100 / n      # 如果 n=0，这里会抛异常
    print(f"成功: {result}")  # 不会执行到这里
except ValueError:
    print("输入不是数字")
# 问题：ZeroDivisionError 没有被捕获！

# 好的写法：用 else 区分"可能出错的代码"和"成功后执行的代码"
try:
    n = int(input("请输入数字: "))
    result = 100 / n
except ValueError:
    print("输入不是数字")
except ZeroDivisionError:
    print("不能除以 0")
else:
    print(f"成功: {result}")  # 只有成功时才执行
```

#### 2.3 finally 收尾（5min）

**finally 的核心特性——无论如何都执行：**

```python
# finally 的四种场景
def demo_finally():
    try:
        print("try: 正在执行...")
        # 这里可以放任何可能出错的代码
    except ValueError:
        print("except: 捕获到异常")
    else:
        print("else: 没有异常")
    finally:
        print("finally: 执行收尾工作")
        print("  - 关闭文件")
        print("  - 释放资源")
        print("  - 断开连接")

# 场景 1：正常执行
demo_finally()
# 输出：
# try: 正在执行...
# else: 没有异常
# finally: 执行收尾工作

# 场景 2：发生异常且被捕获（需要手动制造异常测试）
# try 中抛出 ValueError -> except 执行 -> finally 执行

# 场景 3：发生异常未被捕获 -> finally 执行后才崩溃

# 场景 4：try/except 中有 return -> finally 在 return 之前执行
```

**finally 的实际应用：**

```python
# 1. 关闭文件
f = None
try:
    f = open("data.txt", "r", encoding="utf-8")
    content = f.read()
    print(content)
except FileNotFoundError:
    print("文件不存在")
finally:
    if f:
        f.close()         # 确保文件被关闭
        print("文件已关闭")

# 2. 清理 turtle 画布
import turtle
try:
    t = turtle.Turtle()
    n = int(input("请输入半径: "))
    t.circle(n)
except ValueError:
    print("请输入数字！")
finally:
    print("程序结束")
    turtle.done()
```

#### 2.4 raise 主动抛出异常（3min）

**什么时候需要主动抛异常？**

```python
# 当函数检测到不应该继续执行时，主动抛出异常

def divide(a, b):
    """安全的除法函数"""
    if b == 0:
        raise ValueError("除数不能为 0！")
    if not isinstance(a, (int, float)):
        raise TypeError("被除数必须是数字")
    return a / b

# 使用该函数
try:
    result = divide(10, 0)
except ValueError as e:
    print(f"计算错误: {e}")

try:
    result = divide("abc", 5)
except TypeError as e:
    print(f"类型错误: {e}")

# raise 后跟异常类型或异常对象
raise ValueError("自定义错误信息")
# 或
err = ValueError("自定义错误信息")
raise err
```

**raise 的三种用法：**

| 用法 | 说明 | 示例 |
|------|------|------|
| raise 异常类 | 创建并抛出异常 | raise ValueError |
| raise 异常对象 | 抛出已有异常对象 | raise ValueError("信息") |
| raise（无参数） | 重新抛出当前异常 | 在 except 块中使用 |

---

### 第三段：演示（30:00 - 40:00）

#### 演示 1：保护 turtle 画图程序

```python
import turtle

t = turtle.Turtle()
t.speed(5)
t.pensize(3)
t.pencolor("blue")
turtle.title("异常保护画图程序")

try:
    n = int(input("请输入圆的半径: "))
    t.circle(n)
except ValueError:
    print("请输入数字！")
except turtle.TurtleGraphicsError as e:
    print(f"绘图错误: {e}")
else:
    print(f"成功画了一个半径为 {n} 的圆！")
finally:
    print("程序结束")

turtle.done()
```

**结构拆解：**
- try：获取用户输入并画圆
- except ValueError：输入不是数字时捕获
- except TurtleGraphicsError：绘图异常时捕获
- else：画圆成功时提示
- finally：不管怎样都打印"程序结束"

#### 演示 2：多异常捕获 + raise

```python
def get_positive_number(prompt):
    """获取用户输入的正数，带完整异常保护"""
    try:
        n = float(input(prompt))
        if n <= 0:
            raise ValueError("必须是正数！")
        return n
    except ValueError as e:
        print(f"输入错误: {e}")
        return None

# 使用
radius = get_positive_number("请输入圆的半径: ")
if radius:
    print(f"圆的面积 = {3.14 * radius ** 2:.2f}")
```

#### 演示 3：finally 的四种场景全覆盖

```python
print("=" * 40)
print("场景 1：正常执行")
print("=" * 40)
try:
    print("try: 1 / 2 =", 1 / 2)
except ZeroDivisionError:
    print("except: 除零错误")
else:
    print("else: 计算成功")
finally:
    print("finally: 收尾")
print()

print("=" * 40)
print("场景 2：异常被捕获")
print("=" * 40)
try:
    print("try: 1 / 0 =", 1 / 0)
except ZeroDivisionError:
    print("except: 捕获到除零错误")
else:
    print("else: 计算成功")
finally:
    print("finally: 收尾")
print()

print("=" * 40)
print("场景 3：异常未被捕获")
print("=" * 40)
try:
    print("try:", int("abc"))
except ValueError:
    print("except: 值错误")  # 如果改为 ValueError 会被捕获
finally:
    print("finally: 即使崩溃也会执行")


### 第四段：实操练习（40:00 - 55:00）

#### 练习 1：多 except 填空（5min）

```
题目：补全代码，同时捕获 ValueError 和 ZeroDivisionError

try:
    n = int(input("请输入数字: "))
    result = 100 / n
except ____:
    print("输入不是数字")
except ____:
    print("除数不能为 0")
else:
    print(f"结果是: {result}")
```

**答案：** ValueError, ZeroDivisionError

#### 练习 2：finally 补全（5min）

```
题目：补全代码，确保文件不管是否出错都能关闭

f = None
try:
    f = open("data.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("文件不存在")
____:
    if f:
        f.close()
        print("文件已关闭")
```

**答案：** finally

#### 练习 3：raise 练习（5min）

```
题目：写一个函数 check_age(age)，如果 age < 0 或 age > 150，
      抛出 ValueError
```

**参考答案：**
```python
def check_age(age):
    if age < 0 or age > 150:
        raise ValueError(f"年龄 {age} 不在有效范围(0-150)内")
    print(f"年龄 {age} 有效")

try:
    check_age(200)
except ValueError as e:
    print(f"错误: {e}")
```

---

### 第五段：小测（55:00 - 60:00）

#### 题目 1（选择）
以下哪个关于 finally 的说法是正确的？
A. 只在 try 正常执行时运行
B. 只在 except 捕获异常时运行
C. 无论是否发生异常都会运行
D. 不能和 try 一起使用

**答案：C**

#### 题目 2（选择）
else 分支在什么时候执行？
A. try 块抛出了异常时
B. try 块没有抛出异常时
C. 每次都会执行
D. 只在 finally 之后执行

**答案：B**

#### 题目 3（选择）
raise ValueError("信息") 的作用是？
A. 捕获一个 ValueError 异常
B. 主动抛出一个 ValueError 异常
C. 忽略一个 ValueError 异常
D. 记录一个 ValueError 异常

**答案：B**

#### 题目 4（判断）
一个 try 块后面可以有多个 except 块。

**答案：** 对（每个 except 捕获不同类型的异常）

#### 题目 5（简答）
finally 和 else 的区别是什么？

**答案：** else 只在 try 没有抛出异常时执行（成功后的庆祝），finally 不管有没有异常都执行（必须做的收尾工作）。

---

## 板书设计

```
+-------------------------------------------------------+
|       第08节 异常进阶                                   |
+-------------------------------------------------------+

  完整异常处理结构：
  try:      可能出错的代码
  except A: 捕获 A 类型异常
  except B: 捕获 B 类型异常
  else:     没有异常时执行
  finally:  不管怎样都执行

  raise 主动抛异常：
  raise ValueError("信息")
  raise（重新抛出当前异常）

  记忆钩子：
  try = 抛球  except = 接球
  else = 庆祝  finally = 谢幕
  raise = 自己扔炸弹
---

## 课后延伸

| 项目 | 内容 |
|------|------|
| 课后练习 | 课后练习_第08节.md |
| 预习内容 | 第08节 文件读写 |
| 拓展挑战 | 用异常处理保护一个完整的计算器程序 |
| 家长沟通 | 今天学习了 Python 的异常处理进阶，孩子能写出更健壮的程序了 |

---

## 教学反思

| 维度 | 记录 |
|------|------|
| 学生掌握率 | 预计 65% 学生能独立编写带异常处理的程序 |
| 常见困惑 | else 和 finally 的执行条件容易混淆 |
| 改进方向 | 增加更多"带异常处理的文件操作"练习 |
| 备注 | raise 的概念比较抽象，需要多举例说明使用场景 |
