Skip to content

第 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

读法:

  1. 最后一行:错误类型 + 原因 → 先看这个
  2. 往上找:报错发生在哪个文件、哪一行
  3. 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}")      # 被拦下了:年龄不合法:-5

raise 后面跟异常类型和说明文字,调用方就能优雅处理。

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 练一练 ​

  1. 写一个 safe_div(a, b),除数为 0 或参数不是数字时返回 None 并打印提示。
  2. 读取用户指定的文件名,文件不存在时提示"文件不存在"而不是崩溃。
  3. 写一个函数把字符串转成整数,失败时抛出带说明的 ValueError。
  4. 用 try / except / else / finally 写一段"读文件 + 统计行数 + 打印用时"的代码。

本章关键词 ​

SyntaxError vs 异常、Traceback、try / except / else / finally、raise、自定义异常、raise ... from、with

试图用自己的方式,重新定义属于我的人生