前面我们学了变量、循环、函数,写出的代码能按逻辑运行了 —— 但现实中总会有 “意外”:比如让用户输入数字,他偏输文字;计算时不小心除以零;打开文件时文件被删了…… 这些情况如果不处理,程序会直接崩溃(弹出一堆红色错误),体验很差。
而try-except异常处理机制,就是给程序加 “应急预案”:提前预判可能出问题的地方,一旦出错就执行备用方案,让程序 “优雅容错” 而不是崩溃。今天用生活案例讲透异常处理的逻辑和用法,新手也能轻松掌握。
一、先搞懂:什么是 “异常”?用生活案例类比
生活中处处有 “异常”(意外状况):
- 你计划 “用洗衣机洗衣服”,结果 “停电了”(意外);
- 你想 “打开冰箱拿牛奶”,结果 “牛奶喝完了”(意外);
- 你计算 “购物总价”,结果 “计算器没电了”(意外)。
在 Python 中,异常(Exception)就是程序运行时遇到的错误,比如:
- 输入错误:让用户输数字,他输了 “abc”(ValueError);
- 计算错误:用数字除以 0(ZeroDivisionError);
- 类型错误:用字符串和数字做加法(TypeError);
- 文件错误:打开一个不存在的文件(FileNotFoundError)。
这些异常如果不处理,程序会立刻停止运行,并显示 “Traceback”(错误追踪)信息,比如:
# 错误示例:除以零 a = 10 / 0 # 运行后直接崩溃 |
会弹出红色错误:
Traceback (most recent call last): File "test.py", line 1, in <module> a = 10 / 0 ZeroDivisionError: division by zero # 明确告诉你:除以零错误 |
二、为什么需要异常处理?—— 让程序更 “抗造”
没有异常处理的程序,就像 “玻璃心”:一点小错就崩溃;有了异常处理,程序就有了 “抗压能力”,能在出错时:
- 不崩溃:继续执行后续代码;
- 给提示:告诉用户 “哪里错了,该怎么办”(而不是一堆晦涩的错误代码);
- 留记录:把错误信息保存到日志,方便后续排查问题。
比如一个 “让用户输入数字并计算平方” 的程序,没有异常处理时,用户输文字会崩溃;有了异常处理,会提示 “请输入正确的数字”,并让用户重新输入。
三、异常处理基础:try-except 基本结构(核心)
try-except的逻辑就像 “应急预案”:
预案步骤: 1. 尝试做一件可能出错的事(try块); 2. 如果出错了,就执行备用方案(except块); 3. 如果没出错,就跳过备用方案,继续往下走。 |
1. 语法格式:
try: 可能发生异常的代码块 # 尝试执行这里的代码 except 异常类型: # 预判可能出现的异常类型(比如ZeroDivisionError) 异常发生时执行的代码块 # 出错了就执行这里(备用方案) |
2. 案例 1:处理 “除以零” 异常(ZeroDivisionError)
比如写一个 “计算两数相除” 的程序,提前处理 “除数为 0” 的情况:
def divide(a, b): try: # 尝试执行除法(可能出错的步骤) result = a / b print(f"计算结果:{result}") except ZeroDivisionError: # 如果除数为0,执行这里的备用方案 print("出错了!除数不能为0,请重新输入。") # 测试正常情况(不出错) divide(10, 2) # 输出:计算结果:5.0(try块执行,except块跳过) # 测试异常情况(除数为0) divide(10, 0) # 输出:出错了!除数不能为0,请重新输入。(except块执行,程序不崩溃) |
3. 案例 2:处理 “输入类型错误”(ValueError)
比如让用户输入数字,用户可能输文字,用try-except处理:
try: # 尝试把用户输入的内容转成整数(可能出错:输入不是数字) num = int(input("请输入一个数字:")) print(f"你输入的数字是:{num}") except ValueError: # 输入不是数字时,执行备用方案 print("输入错误!请输入一个有效的数字(比如123)。") # 测试正常输入(输数字) # 请输入一个数字:100 → 输出:你输入的数字是:100(try块执行) # 测试异常输入(输文字) # 请输入一个数字:abc → 输出:输入错误!请输入一个有效的数字(比如123)。(except块执行) |
四、捕获多个异常:针对性处理不同错误
一个程序可能出现多种异常(比如既可能除数为 0,又可能输入不是数字),可以用多个except块分别处理,就像 “针对不同意外准备不同预案”。
1. 语法格式:
try: 可能发生异常的代码块 except 异常类型1: # 处理第一种异常 处理代码1 except 异常类型2: # 处理第二种异常 处理代码2 except (异常类型3, 异常类型4): # 用元组把多个异常放一起,用同一套处理逻辑 处理代码3 |
2. 案例:一个程序处理 3 种异常
写一个 “让用户输入两个数字,计算除法” 的程序,可能出现 3 种错误:
- 输入不是数字(ValueError);
- 除数为 0(ZeroDivisionError);
- 其他未知错误(用Exception捕获,它是所有异常的父类)。
try: # 步骤1:输入第一个数字(可能出错:非数字) num1 = int(input("请输入被除数:")) # 步骤2:输入第二个数字(可能出错:非数字) num2 = int(input("请输入除数:")) # 步骤3:计算除法(可能出错:除数为0) result = num1 / num2 print(f"{num1} ÷ {num2} = {result}") except ValueError: # 处理“输入不是数字”的错误 print("输入错误!请确保输入的是整数(比如10、20)。") except ZeroDivisionError: # 处理“除数为0”的错误 print("计算错误!除数不能为0。") except Exception as e: # 捕获其他所有未预料到的错误,用as e获取错误详情 # 处理未知错误(调试时很有用) print(f"发生了未知错误:{e}") # e是错误的具体信息 # 测试1:输入非数字(比如输入"abc") # 输出:输入错误!请确保输入的是整数(比如10、20)。 # 测试2:除数为0(比如被除数10,除数0) # 输出:计算错误!除数不能为0。 # 测试3:其他错误(比如num1 = 10,num2 = "2" → 但这里num2已转成int,所以不会,仅举例) # 输出:发生了未知错误:...(具体错误信息) |
关键:异常类型的 “父子关系”
Exception是所有内置异常的 “父类”,如果把except Exception放在最前面,会 “拦截” 所有异常,后面的except块永远不会执行(因为父类异常会先捕获)。
正确顺序:先写具体的异常(子类),最后写Exception(父类)兜底,比如:
# 正确顺序:具体异常在前,父类异常在后 try: # ... except ValueError: # 处理ValueError except ZeroDivisionError: # 处理ZeroDivisionError except Exception: # 处理其他异常(兜底) |
五、else 子句:没出错时执行的代码
如果想在 “没发生异常” 时执行一些代码(比如 “操作成功” 的提示),可以用else子句,它和try-except配合使用,逻辑更清晰。
语法格式:
try: 可能出错的代码 except 异常类型: 出错时执行的代码 else: 没出错时执行的代码 # 只有try块没异常,才会执行这里 |
案例:计算成功时提示 “操作完成”
try: num1 = int(input("请输入被除数:")) num2 = int(input("请输入除数:")) result = num1 / num2 except ValueError: print("输入错误:请输入数字。") except ZeroDivisionError: print("计算错误:除数不能为0。") else: # 只有上面没出错,才会执行这里 print(f"计算成功!{num1} ÷ {num2} = {result}") print("操作完成~") # 测试正常输入(10和2): # 输出:计算成功!10 ÷ 2 = 5.0 → 操作完成~ # 测试异常输入(输"abc"): # 输出:输入错误:请输入数字。(else块不执行) |
六、finally 子句:无论是否出错都必须执行的代码
有些操作 “无论是否出错都必须执行”(比如打开文件后必须关闭,连接数据库后必须断开),用finally子句,它永远会执行,即使try或except里有return。
语法格式:
try: 可能出错的代码 except 异常类型: 出错时执行的代码 finally: 无论是否出错,都会执行的代码 # 必执行,比如清理资源 |
案例:模拟 “打开文件后必须关闭”
def read_file(): file = None # 先定义变量,避免except块里引用未定义的变量 try: # 尝试打开文件(可能出错:文件不存在) file = open("test.txt", "r") # 假设test.txt不存在 content = file.read() print(f"文件内容:{content}") except FileNotFoundError: print("出错了:文件不存在。") finally: # 无论是否出错,都要关闭文件(如果文件已打开) if file is not None: # 避免file未赋值时调用close() file.close() print("文件已关闭(即使出错也执行)") read_file() # 运行结果(test.txt不存在时): # 出错了:文件不存在。 # 文件已关闭(即使出错也执行) |
为什么需要finally?
如果没有finally,关闭文件的代码要在try和except里各写一次(冗余);有了finally,只写一次即可,保证 “清理操作” 必执行。
七、主动抛出异常:raise 关键字
除了处理 “程序自动产生的异常”,我们还能 “主动抛出异常”(比如验证用户输入时,发现年龄为负数,主动提示错误),用raise关键字。
语法格式:
if 不符合条件的情况: raise 异常类型("错误提示信息") # 主动抛出异常 |
案例:验证年龄必须为正数
def check_age(age): if age < 0: # 主动抛出ValueError,并附带提示信息 raise ValueError("年龄不能为负数!请输入大于等于0的数字。") print(f"年龄验证通过:{age}岁") # 测试正常情况(年龄18) check_age(18) # 输出:年龄验证通过:18岁 # 测试异常情况(年龄-5) try: check_age(-5) # 主动抛出的异常,需要用try-except捕获 except ValueError as e: print(f"捕获到错误:{e}") # 输出:捕获到错误:年龄不能为负数!请输入大于等于0的数字。 |
作用:提前暴露错误,避免后续逻辑出错
比如计算 “出生年份 = 当前年份 - 年龄”,如果年龄是负数,不主动抛出异常,后续会算出 “2023 - (-5) = 2028” 这种荒谬结果;主动抛出异常,能在源头阻止错误。
八、新手易错点总结
- except 块顺序错误:
把except Exception(父类异常)放在前面,会导致后面的具体异常(如ValueError)永远不执行,正确顺序是 “具体异常在前,父类异常在后”。
- 空 except 块(吞噬错误):
写except:不指定异常类型,会捕获所有异常(包括KeyboardInterrupt这种用户中断程序的操作),而且不做任何处理,导致错误被隐藏,难以调试。
❌ 错误示例:
try: 10 / 0 except: # 空except,吞噬错误 pass # 什么都不做 |
- finally 里用 return:
finally里的return会覆盖try或except里的return,导致返回值不符合预期,尽量别在finally里用return。
- 未捕获的异常:
只处理了部分异常,遗漏了其他可能的错误(比如只处理ValueError,没处理TypeError),导致程序仍可能崩溃,建议最后用except Exception兜底(但要记录错误信息)。
- 主动抛异常后没捕获:
用raise抛出异常后,如果不在调用处用try-except捕获,程序会直接崩溃,记得 “抛异常的地方” 和 “用异常的地方” 要配合。
九、练手小任务(动手巩固!)
- 任务 1:写一个 “安全除法” 函数
函数接收两个参数a和b,返回a/b的结果。要求:
-
- 如果b为 0,返回None并提示 “除数不能为 0”;
- 如果a或b不是数字(比如字符串),返回None并提示 “参数必须是数字”;
- 其他错误返回None并提示 “发生未知错误”。
- 任务 2:处理用户输入的年龄,直到输入合法
循环让用户输入年龄,要求:
-
- 年龄必须是整数,且在 0-120 之间;
- 如果输入不是整数,提示 “请输入数字”;
- 如果年龄 <0 或> 120,主动抛出ValueError并提示 “年龄必须在 0-120 之间”;
- 输入合法则退出循环,打印 “年龄输入正确:xx 岁”。
- 任务 3:模拟文件读写的异常处理
写一个函数read_and_write(input_file, output_file),功能:
-
- 读取input_file的内容(假设文件内容是字符串);
- 把内容写入output_file;
- 处理 “输入文件不存在”(FileNotFoundError)、“没有写入权限”(PermissionError)等异常;
- 无论是否出错,都打印 “操作结束”。
下一篇我们会讲 “Python 基础语法 5:模块与包”,学习如何用别人写好的代码(比如math库、random库),以及如何组织自己的代码,让程序结构更清晰。如果练手时遇到问题,评论区留言,我会帮你分析解决!