Python零基础入门(五):异常处理
前言
前四篇我们完成了环境搭建与数据类型、运算符与条件判断、面向对象编程基础和模块与包导入。你已经能让程序"算"、能"想"、能"组织"——但程序运行中总会遇到各种意外:文件不存在、网络连接失败、除以零、用户输入非法数据……
这些"意外"就是异常。如果不对异常进行处理,程序会直接崩溃并抛出错误信息。一个健壮的程序,必须能够优雅地应对各种异常情况,而不是一遇到错误就"罢工"。
本篇将系统讲解 Python 的异常处理机制,让你的程序更加稳定可靠。
一、什么是异常?
异常(Exception) 是程序运行过程中发生的错误事件。当 Python 遇到无法正常执行的情况时,会"抛出"一个异常对象。如果这个异常没有被捕获处理,程序就会终止并显示错误信息(称为"堆栈跟踪")。
1.1 异常示例
# 没有异常处理的代码def divide(a, b): return a / bresult = divide(10, 0) # ZeroDivisionError: division by zeroprint("这行永远不会执行")
程序在第 3 行就崩溃了,第 4 行根本不会执行。运行结果:
Traceback (most recent call last): File "main.py", line 3, in <module> result = divide(10, 0)ZeroDivisionError: division by zero
1.2 常见内置异常
Python 内置了丰富的异常类型,每种类型对应一种特定的错误情况:
| | |
|---|
ValueError | | int("abc") |
TypeError | | "hello" + 5 |
KeyError | | dict["不存在的键"] |
IndexError | | list[100] |
FileNotFoundError | | open("不存在.txt") |
ZeroDivisionError | | 1/0 |
AttributeError | | "hello".foo() |
ImportError | | import 不存在的模块 |
NameError | | print(未定义的变量) |
RuntimeError | | |
二、try-except 基本语法
Python 使用 try-except 语句来捕获和处理异常:
try: # 可能发生异常的代码 result = 10 / 0except ZeroDivisionError: # 处理特定异常 print("错误:除数不能为零!")
2.1 捕获特定异常
try: num = int(input("请输入数字:")) result = 100 / numexcept ValueError: print("输入的不是有效数字!")except ZeroDivisionError: print("不能除以零!")
可以有多个 except 块,分别处理不同类型的异常。Python 会从上到下依次检查,匹配到第一个就执行,然后跳过剩余的 except 块。
2.2 捕获多个异常
如果多个异常的处理方式相同,可以合并写:
try: # 可能发生多种异常的代码 value = int(input("输入数字:")) result = 100 / valueexcept (ValueError, ZeroDivisionError) as e: print(f"发生错误:{e}")
2.3 捕获异常对象
使用 as 关键字可以获取异常对象,获取详细的错误信息:
try: with open("不存在的文件.txt", "r") as f: content = f.read()except FileNotFoundError as e: print(f"文件不存在:{e}") # 输出:文件不存在:[Errno 2] No such file or directory: '不存在的文件.txt'
三、else 和 finally
除了 try 和 except,Python 还提供了 else 和 finally 子句:
try: num = int(input("请输入数字:")) result = 100 / numexcept ValueError: print("输入无效")except ZeroDivisionError: print("不能除以零")else: # 没有发生异常时执行 print(f"结果是:{result}")finally: # 无论是否发生异常都会执行(通常用于清理工作) print("程序结束")
3.1 执行流程
- • except 块:发生异常时执行(可以有多个)
- • finally 块:无论如何都会执行(可选,常用于资源清理)
3.2 else 的使用场景
else 子句适用于"只有在没有异常时才执行"的代码,这样可以把可能抛出异常的代码限制在最小范围:
try: f = open("data.txt", "r")except FileNotFoundError: print("文件不存在")else: # 只有文件成功打开后才读取 content = f.read() f.close() print(content)
3.3 finally 的使用场景
finally 子句无论如何都会执行,即使在 try 或 except 中有 return 语句。它常用于资源清理:
def read_file(filename): f = None try: f = open(filename, "r") return f.read() except FileNotFoundError: return "文件不存在" finally: # 无论成功还是失败,都确保文件被关闭 if f: f.close() print("文件已关闭")
四、万能异常捕获(谨慎使用)
使用 Exception 可以捕获几乎所有非系统退出异常:
try: # 任意可能出错的代码 risky_operation()except Exception as e: # 捕获所有非系统退出异常 print(f"发生了错误:{type(e).__name__}: {e}")
4.1 为什么不推荐滥用?
# 最糟糕的做法:静默吞掉异常try: dangerous_operation()except: pass # 异常被忽略了,出了问题你根本不知道!
滥用 except Exception 的问题:
- 2. 难以调试:程序"看起来正常",但结果可能是错的
- 3. 掩盖 bug:可能让程序在错误状态下继续运行
4.2 何时使用?
只在以下场景使用 except Exception:
- • 最外层错误处理:捕获所有未预料到的异常,记录日志后安全退出
import loggingdef safe_operation(): try: # 复杂的业务逻辑 result = complex_calculation() return result except Exception as e: # 记录日志 logging.error(f"操作失败:{e}", exc_info=True) # 重新抛出,让调用者决定如何处理 raise
五、raise 主动抛出异常
除了被动捕获异常,你还可以使用 raise 关键字主动抛出异常:
5.1 基本用法
def set_age(age): if age < 0 or age > 150: raise ValueError(f"年龄必须在 0-150 之间,当前值:{age}") return agetry: set_age(200)except ValueError as e: print(f"错误:{e}") # 输出:错误:年龄必须在 0-150 之间,当前值:200
5.2 重新抛出异常
在捕获异常后,可以用 raise 重新抛出(不带参数):
try: result = 10 / 0except ZeroDivisionError: print("记录日志:除零错误") raise # 重新抛出,让上层处理
5.3 异常链(Exception Chaining)
在捕获一个异常后抛出另一个异常,可以用 raise ... from ... 保留原始异常信息:
def convert_to_int(value): try: return int(value) except ValueError as e: raise TypeError(f"无法转换为整数:{value}") from etry: convert_to_int("abc")except TypeError as e: print(f"错误:{e}") print(f"原始异常:{e.__cause__}")
六、自定义异常
当内置异常不能准确描述错误时,可以创建自己的异常类:
6.1 基本定义
class InsufficientFundsError(Exception): """余额不足异常""" pass# 使用balance = 100amount = 150if amount > balance: raise InsufficientFundsError(f"余额不足:需要 {amount},当前余额 {balance}")
6.2 带属性的自定义异常
class InsufficientFundsError(Exception): """余额不足异常""" def __init__(self, balance, amount): self.balance = balance self.amount = amount self.deficit = amount - balance super().__init__(f"余额不足:需要 {amount},当前余额 {balance},缺少 {self.deficit}")class BankAccount: def __init__(self, balance=0): self.balance = balance def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount return self.balance# 使用account = BankAccount(100)try: account.withdraw(150)except InsufficientFundsError as e: print(f"取款失败:{e}") print(f"缺少金额:{e.deficit}")
6.3 自定义异常的最佳实践
- 1. 继承
Exception类(而不是 BaseException) - 2. 命名以
Error结尾(如 ValidationError、TimeoutError) - 5. 组织异常层次:定义一个基类,然后派生具体异常
# 异常层次示例class AppError(Exception): """应用异常基类""" passclass ValidationError(AppError): """数据验证错误""" passclass DatabaseError(AppError): """数据库错误""" passclass ConnectionError(DatabaseError): """数据库连接错误""" pass
七、异常处理最佳实践
7.1 EAFP 原则
Python 社区推崇 EAFP(Easier to Ask Forgiveness than Permission,请求原谅比请求许可更容易)风格:
# LBYL(Look Before You Leap)风格 —— 先检查再操作if key in dictionary: value = dictionary[key]else: value = default_value# EAFP 风格 —— 先操作,出错再处理(更 Pythonic)try: value = dictionary[key]except KeyError: value = default_value
EAFP 的优势:
- 3. 符合 Python 风格:Python 内置的很多操作都遵循这个原则
7.2 具体异常优于宽泛异常
# 不好:捕获所有异常try: result = int(user_input)except Exception: print("出错了")# 好:捕获特定异常try: result = int(user_input)except ValueError: print("请输入有效的数字")
7.3 不要忽略异常
# 最糟糕的做法try: dangerous_operation()except: pass# 至少记录一下import loggingtry: dangerous_operation()except Exception as e: logging.error(f"操作失败:{e}")
7.4 合理使用 finally
# finally 用于资源清理file = Nonetry: file = open("data.txt", "r") content = file.read() process(content)except FileNotFoundError: print("文件不存在")finally: if file: file.close()# 更好的方式:使用 with 语句(下一篇会详细讲)with open("data.txt", "r") as file: content = file.read()
7.5 保持异常粒度适中
# 不好:一个 try 包含太多代码try: data = read_file(filename) parsed = parse_data(data) result = process(parsed) save_result(result)except Exception: print("出错了") # 哪一步出错?不知道!# 好:每个可能出错的操作单独处理try: data = read_file(filename)except FileNotFoundError: print("文件不存在") returntry: parsed = parse_data(data)except ParseError: print("数据格式错误") return# ... 以此类推
八、综合实战:健壮的用户输入处理
让我们结合所学知识,实现一个健壮的用户输入处理函数:
class InputError(Exception): """输入错误基类""" passclass OutOfRangeError(InputError): """数值超出范围""" def __init__(self, value, min_val, max_val): self.value = value self.min_val = min_val self.max_val = max_val super().__init__(f"数值 {value} 超出范围 [{min_val}, {max_val}]")class InvalidFormatError(InputError): """格式无效""" passdef get_integer(prompt, min_val=None, max_val=None): """ 获取用户输入的整数,带验证 Args: prompt: 提示信息 min_val: 最小值(可选) max_val: 最大值(可选) Returns: int: 验证通过的整数 Raises: InvalidFormatError: 输入不是有效整数 OutOfRangeError: 数值超出范围 """ while True: try: user_input = input(prompt).strip() if not user_input: raise InvalidFormatError("输入不能为空") value = int(user_input) if min_val is not None and value < min_val: raise OutOfRangeError(value, min_val, max_val or float('inf')) if max_val is not None and value > max_val: raise OutOfRangeError(value, min_val or float('-inf'), max_val) return value except ValueError: print("错误:请输入有效的整数") except InvalidFormatError as e: print(f"错误:{e}") except OutOfRangeError as e: print(f"错误:{e}") except KeyboardInterrupt: print("\n用户取消输入") raise SystemExitdef get_choice(prompt, options): """ 获取用户选择 Args: prompt: 提示信息 options: 有效选项列表 Returns: str: 用户选择的选项 """ while True: try: choice = input(prompt).strip().lower() if choice not in options: raise InvalidFormatError(f"无效选择,请输入:{', '.join(options)}") return choice except InvalidFormatError as e: print(f"错误:{e}")# 使用示例if __name__ == "__main__": print("=== 用户信息录入 ===") # 获取年龄(1-150) age = get_integer("请输入年龄:", min_val=1, max_val=150) print(f"年龄:{age}") # 获取分数(0-100) score = get_integer("请输入分数:", min_val=0, max_val=100) print(f"分数:{score}") # 获取选择 action = get_choice("请选择操作 (y/n): ", ["y", "n", "yes", "no"]) print(f"选择:{action}") print("录入完成!")
这个例子展示了:
九、动手练习
- 1. 写一个函数
safe_divide(a, b),当除数为 0 时返回 None 而不是抛出异常。 - 2. 创建一个自定义异常
InvalidEmailError,当邮箱格式不正确时抛出(只需检查是否包含 @ 和 .)。 - 3. 写一个文件读取函数,处理文件不存在、权限不足、编码错误等异常,返回友好的错误信息。
- 4. 实现一个简单的计算器,处理除零错误、无效输入、不支持的运算符等异常。
- 5. 创建一个"重试装饰器",当函数抛出异常时自动重试指定次数。
十、小结
本篇我们系统学习了 Python 的异常处理机制:
核心语法
自定义异常
最佳实践
- • 具体异常优于宽泛异常:不要滥用
except Exception
异常处理是编写健壮程序的关键技能。掌握了它,你的程序就能优雅地应对各种意外情况,而不是一遇到错误就崩溃。
下一篇,我们将学习文件操作——让你的程序能读写数据、处理配置文件、生成报告。不见不散!
本文首发于微信公众号「羽您码上聊」,欢迎关注获取更多技术内容。