深色模式
第 08 章 · 错误与异常:让程序摔了也不散架
本章目标
看懂报错、接住报错、自己抛出报错,知道程序出错时怎么收尾。
8.1 两种"错"要分清
语法错误(SyntaxError):代码本身写错了,Python 读都读不懂,一行都不会执行。
python
print("hello" # ❌ SyntaxError: '(' was never closed异常(Exception):语法没问题,但运行时出了状况。程序跑到那一行才崩。
python
print(1 / 0) # ❌ ZeroDivisionError: division by zero绝大多数报错属于前者——打错字、少了冒号括号引号、缩进不对。
8.2 读报错:从下往上看
text
Traceback (most recent call last):
File "demo.py", line 7, in <module>
main()
File "demo.py", line 4, in main
print(10 / n)
ZeroDivisionError: division by zero读法:
- 最后一行:错误类型 + 原因 → 先看这个
- 往上找:报错发生在哪个文件、哪一行
Traceback里的调用链是"谁调用了谁",最下面一行通常就是出事的地方
8.3 try / except:接住异常
python
try:
n = int(input("请输入一个数字:"))
print(f"100 ÷ {n} = {100 / n:.2f}")
except ValueError:
print("输入的不是数字")
except ZeroDivisionError:
print("除数不能为 0")一次接不住,程序就整个崩掉;接住以后,程序可以继续往下走。
完整结构(四段):
python
try:
with open("data.txt", encoding="utf-8") as f: # 用 with,异常也会自动关文件
data = f.read()
except FileNotFoundError:
print("文件不存在,用默认值代替")
data = ""
except Exception as e: # 其它没预料到的错误:记录下来
print(f"出错了:{type(e).__name__}: {e}")
data = ""
else:
print("try 里没出错,才执行这里") # 正常分支
finally:
print("不管成功失败都执行") # 收尾:关连接、删临时文件、写日志四个关键字的顺序必须是这样,else 和 finally 都可以省略。
else 和 finally 到底有什么用?(分开看更清楚)
python
# else:区分"没出错时做的事"和"只在出错时做的事"
def read_config(path):
try:
with open(path, encoding="utf-8") as f:
text = f.read() # 注意:不能在 try 里直接 return,否则 else 永远不会执行
except FileNotFoundError:
print(f"{path} 不存在,用默认配置")
return "{}"
else:
print("配置读取成功") # try 里没出错,才会走到这里
return text
# finally:无论成败都必须做的收尾
def timing_demo():
import time
t0 = time.time()
try:
sum(range(100000))
finally:
print(f"耗时 {time.time() - t0:.4f} 秒") # 即使中途报错也会打印⚠️ 别把"关文件"写在
else里:文件已经在try里打开、读取时抛了异常,else就不会执行,文件句柄会漏掉。用with或把close()放finally。
⚠️ 别写裸的 except: 或 except Exception: pass——它会把所有错误都吞掉,包括你自己的笔误,结果程序"看起来没事但结果不对",极难查。要捕获就写具体的异常类型。
8.4 自己抛出异常:raise
库里、函数里发现"参数不合理",应该主动报错,而不是返回一个莫名其妙的值:
python
def set_age(age):
if not isinstance(age, int):
raise TypeError("age 必须是整数")
if age < 0 or age > 150:
raise ValueError(f"年龄不合法:{age}")
return age
try:
set_age(-5)
except ValueError as e:
print(f"被拦下了:{e}") # 被拦下了:年龄不合法:-5raise 后面跟异常类型和说明文字,调用方就能优雅处理。
8.5 自定义异常
项目大了,用自定义异常能区分"是我这套系统的问题":
python
class BalanceNotEnoughError(Exception):
"""余额不足"""
def withdraw(balance, amount):
if amount > balance:
raise BalanceNotEnoughError(f"余额 {balance},想取 {amount}")
return balance - amount继承 Exception 就行,类里可以什么都不写。
8.6 异常链:raise ... from
处理一个错误时又抛出另一个,用 from 保留原委:
python
import json
text = '{"a": 1,,}'
try:
config = json.loads(text)
except json.JSONDecodeError as e:
raise ValueError("配置文件格式不对") from e这样报错信息里既有"配置格式不对",也能看到底层原因。
8.7 同时处理多个不相关的异常
一段代码可能出几种错,用元组一次接住:
python
try:
do_something()
except (ValueError, KeyError, TypeError) as e:
print(f"数据处理出错:{e}")8.8 用 with 做"自动清理"(预定义的清理操作)
with 就是最常用的清理机制:
python
with open("a.txt", "w", encoding="utf-8") as f:
f.write("hi")
# 离开 with 块,文件自动关闭,即使中间抛异常也照样关想给自己的类也加上这种能力,实现两个方法:
python
import time
class Timer:
def __enter__(self):
self.t0 = time.time()
return self
def __exit__(self, exc_type, exc, tb):
print(f"耗时 {time.time() - self.t0:.4f} 秒")
return False # False = 不吞掉异常
with Timer():
sum(range(1000000))知道"with 会自动收尾"即可,自定义上下文管理器属进阶内容。
8.9 异常类型速查表
| 异常 | 什么时候出现 | 大概率原因 |
|---|---|---|
SyntaxError | 语法不对 | 少括号/引号/冒号,中文标点 |
IndentationError | 缩进不对 | 空格数量不一致、混用 Tab |
NameError | 名字没定义 | 变量名拼错、忘了先赋值 |
TypeError | 类型用错 | "18" + 1、函数参数个数不对 |
ValueError | 值不合法 | int("abc") |
KeyError | 字典没这个键 | 用 [键] 前先 in 判断或用 .get() |
IndexError | 下标越界 | 列表只有 3 个元素却取 [5] |
AttributeError | 对象没这个属性 | 拼错方法名,如 str.upperr() |
ModuleNotFoundError | 模块找不到 | 没装库 / 文件名撞名 |
FileNotFoundError | 文件不存在 | 路径不对 |
UnboundLocalError | 局部变量未赋值就用 | 忘了 global |
ZeroDivisionError | 除以 0 | 分母没做判断 |
8.10 练一练
- 写一个
safe_div(a, b),除数为 0 或参数不是数字时返回None并打印提示。 - 读取用户指定的文件名,文件不存在时提示"文件不存在"而不是崩溃。
- 写一个函数把字符串转成整数,失败时抛出带说明的
ValueError。 - 用
try / except / else / finally写一段"读文件 + 统计行数 + 打印用时"的代码。
本章关键词
SyntaxError vs 异常、Traceback、try / except / else / finally、raise、自定义异常、raise ... from、with