【Python学习】Python异常处理及程序调试
目录
前言
在Python编程过程中,无论代码编写多么严谨,都难免出现错误。这些错误分为两类:语法错误和异常(运行时错误)。语法错误是代码不符合Python语法规则导致的,编写阶段即可发现;而异常是代码语法正确,但运行时因逻辑漏洞、资源不足等问题引发的错误,若未妥善处理,会导致程序崩溃。
程序调试则是定位并解决这些错误(尤其是异常)的过程,是每个Python开发者必备的核心技能。本教程将从异常处理的基础概念、语法用法,到调试工具、调试技巧,结合大量实操案例,帮助大家系统掌握Python异常处理与程序调试,提升代码的健壮性和可维护性。

第一部分:Python异常处理
1.1 异常的概念与常见类型
1.1.1 什么是异常
异常(Exception)是Python程序运行时发生的非预期事件,会打断程序的正常执行流程。例如:除以零、访问不存在的变量、打开不存在的文件、网络连接失败等,都会触发异常。
Python中所有异常都继承自BaseException类,其核心子类包括Exception(普通异常)和SystemExit(程序退出)、KeyboardInterrupt(用户中断)等。其中,我们日常处理的异常主要是Exception类的子类。
1.1.2 常见异常类型及场景
以下是Python开发中最常遇到的异常类型,结合具体场景说明,方便大家快速识别:
-
ZeroDivisionError(除以零异常):当除数为0时触发。例如:
10 / 0。 -
TypeError(类型错误):操作或函数应用于不适当类型的对象时触发。例如:将字符串与数字相加(
"abc" + 123)、调用非可调用对象(如给变量赋值后误当作函数调用)。 -
IndexError(索引错误):访问列表、元组等序列中不存在的索引时触发。例如:
lst = [1,2,3],执行lst[3]。 -
KeyError(键错误):访问字典中不存在的键时触发。例如:
dict1 = {"name": "Python"},执行dict1["age"]。 -
ValueError(值错误):传入的值类型正确,但值不符合要求时触发。例如:
int("abc")(字符串无法转换为整数)、float("12a3")。 -
FileNotFoundError(文件未找到异常):打开不存在的文件时触发。例如:
open("test.txt", "r"),若当前目录无test.txt文件。 -
ImportError(导入错误):无法导入模块或模块中的对象时触发。例如:导入不存在的模块(
import non_existent_module)、模块中不存在的函数。 -
AttributeError(属性错误):访问对象不存在的属性或方法时触发。例如:
str1 = "hello",执行str1.append("world")(字符串无append方法)。
1.2 异常处理的核心语法
Python提供了try-except语句来捕获并处理异常,避免程序崩溃。核心逻辑是:将可能触发异常的代码放在try块中,若发生异常,执行except块中的处理逻辑;若未发生异常,跳过except块,继续执行后续代码。
1.2.1 基本语法(try-except)
try:
# 可能触发异常的代码(核心执行代码)
代码块1
except 异常类型1:
# 捕获到“异常类型1”时执行的处理逻辑
代码块2
案例:处理除以零异常
try:
a = 10
b = 0
result = a / b # 触发ZeroDivisionError
except ZeroDivisionError:
print("错误:除数不能为0,请重新输入除数!")
# 程序不会崩溃,继续执行后续代码
print("程序继续运行...")
运行结果:
错误:除数不能为0,请重新输入除数! 程序继续运行...
1.2.2 捕获多个异常
实际开发中,一段代码可能触发多种异常,可通过两种方式捕获多个异常:
方式1:多个except子句(推荐,针对性处理)
try:
# 可能触发多种异常的代码
lst = [1, 2, 3]
result = 10 / lst[3] # 可能触发IndexError或ZeroDivisionError
except IndexError:
print("错误:索引超出范围!")
except ZeroDivisionError:
print("错误:除数不能为0!")
except Exception as e: # 捕获所有其他Exception类型异常(兜底)
print(f"发生未知错误:{e}")
说明:except子句的顺序很重要,子类异常必须放在父类异常前面(如IndexError是Exception的子类,需放在Exception前面),否则子类异常会被父类异常捕获,无法触发针对性处理。
方式2:单个except捕获多个异常(用元组包裹)
try:
lst = [1, 2, 3]
result = 10 / lst[3]
except (IndexError, ZeroDivisionError) as e: # 用元组包裹多个异常类型
print(f"发生错误:{e}")
except Exception as e:
print(f"未知错误:{e}")
注:as e用于获取异常信息,方便排查问题,e是异常对象,可直接打印输出错误详情。
1.2.3 try-except-else语句
else子句可选,用于执行“当try块中没有发生异常时”的逻辑,即:try块执行成功后,才会执行else块。
try:
a = 10
b = 2
result = a / b
except ZeroDivisionError:
print("除数不能为0!")
else:
print(f"计算成功,结果为:{result}") # 只有try块无异常时执行
print("程序结束")
运行结果:
计算成功,结果为:5.0 程序结束
1.2.4 try-except-finally语句
finally子句可选,无论try块是否发生异常,finally块都会执行,常用于释放资源(如关闭文件、关闭网络连接等),避免资源泄露。
try:
# 尝试打开文件
f = open("test.txt", "r", encoding="utf-8")
content = f.read()
print(content)
except FileNotFoundError:
print("错误:文件未找到!")
finally:
# 无论是否发生异常,都关闭文件
if 'f' in locals(): # 判断文件对象是否存在
f.close()
print("文件操作结束,资源已释放")
说明:即使try块触发异常,except块执行后,依然会执行finally块;若try块无异常,执行完else(若有)后,也会执行finally块。
1.2.5 raise语句(主动抛出异常)
除了Python自动触发的异常,我们也可以通过raise语句主动抛出异常,用于自定义异常场景(如参数校验不通过)。
def check_age(age):
if age < 0 or age > 120:
# 主动抛出ValueError,并指定异常信息
raise ValueError("年龄必须在0-120之间!")
return f"年龄合法:{age}岁"
try:
check_age(150)
except ValueError as e:
print(f"参数错误:{e}")
运行结果:
参数错误:年龄必须在0-120之间!
也可以重新抛出异常(不修改异常信息),用于多层异常处理时,向上传递异常:
try:
check_age(150)
except ValueError as e:
print("捕获到年龄异常,向上传递...")
raise # 重新抛出当前异常,不改变异常信息
1.3 自定义异常
Python内置的异常类型可覆盖大部分场景,但在实际开发中,我们可能需要定义符合业务场景的异常(如“用户不存在异常”“订单超时异常”)。自定义异常需继承Exception类(不建议继承BaseException,避免捕获到系统级异常)。
1.3.1 自定义异常的基本写法
# 自定义异常类,继承Exception
class UserNotFoundError(Exception):
# 重写__init__方法,自定义异常信息
def __init__(self, user_id):
self.user_id = user_id
# 调用父类的__init__方法,传递异常信息
super().__init__(f"用户ID:{user_id} 不存在!")
# 使用自定义异常
def get_user(user_id):
# 模拟业务逻辑:用户ID不在列表中则抛出异常
user_list = [101, 102, 103]
if user_id not in user_list:
raise UserNotFoundError(user_id)
return f"找到用户,ID:{user_id}"
try:
get_user(104)
except UserNotFoundError as e:
print(f"业务异常:{e}")
运行结果:
业务异常:用户ID:104 不存在!
1.3.2 自定义异常的应用场景
自定义异常主要用于业务逻辑的异常区分,让代码更具可读性和可维护性。例如:在电商系统中,可定义OrderTimeoutError(订单超时)、StockInsufficientError(库存不足)等异常,便于后续定位问题和处理不同的业务异常场景。
1.4 异常处理的最佳实践
-
不要捕获所有异常:避免使用
except Exception:兜底所有异常,应针对性捕获具体异常,否则会掩盖真正的错误(如系统级异常),难以排查。 -
异常信息要明确:捕获异常时,打印具体的异常信息(如
print(f"错误:{e}")),避免只打印“发生错误”这类模糊提示。 -
及时释放资源:涉及文件、网络连接、数据库连接等资源操作时,务必用finally块释放资源,或使用上下文管理器(
with语句)自动释放。 -
自定义异常要规范:自定义异常需继承Exception类,命名规范(以Error结尾),并添加清晰的异常描述,便于团队协作。
-
避免过度使用异常:异常用于处理“非预期错误”,不要用异常替代正常的逻辑判断(如判断参数是否为空,应先做逻辑判断,而非依赖异常捕获)。
第二部分:Python程序调试
程序调试是定位并解决代码中错误(语法错误、异常)的过程。调试的核心是“找到错误的位置、分析错误的原因、修改错误并验证”。Python提供了多种调试方式,从简单的print打印,到专业的调试工具,适用于不同的场景。
2.1 调试的核心思路
无论使用哪种调试方式,核心思路都是一致的:
-
定位错误:确定错误发生的代码行、错误类型(语法错误/异常)。
-
分析原因:结合错误信息,分析代码逻辑、变量值、输入输出是否符合预期。
-
修改错误:根据原因修改代码,避免引入新的错误。
-
验证结果:运行代码,确认错误已解决,且程序运行符合预期。
2.2 常见调试方式(从简单到复杂)
2.2.1 print打印调试(最基础,适合简单代码)
print调试是最直观、最基础的调试方式,通过打印关键变量的值、代码执行的流程,判断错误位置。适用于代码量少、逻辑简单的场景。
案例:调试一个计算函数的错误
def calculate(a, b):
# 调试:打印输入参数的值
print(f"输入参数:a={a}, b={b}")
result = a + b # 假设此处逻辑错误,应为a * b
# 调试:打印计算结果
print(f"计算结果:{result}")
return result
# 调用函数,发现结果不符合预期
print(calculate(3, 4)) # 预期结果12,实际输出7
通过print打印,可快速发现:计算逻辑错误(应为乘法而非加法),修改后即可解决问题。
优点:简单、无需额外工具;缺点:代码中会残留大量print语句,调试完成后需手动删除,适合简单场景。
2.2.2 断言(assert)调试(适合逻辑校验)
断言(assert)是用于判断某个条件是否成立的语句,若条件不成立,会触发AssertionError异常,中断程序运行,并输出指定的提示信息。适用于调试阶段的逻辑校验(如参数合法性、变量值范围)。
语法:assert 条件表达式, 异常提示信息
def calculate(a, b):
# 断言:a和b必须是正数,否则触发AssertionError
assert a > 0 and b > 0, "a和b必须为正数!"
result = a * b
return result
# 调用函数,传入负数,触发断言异常
print(calculate(-3, 4))
运行结果(触发异常):
AssertionError: a和b必须为正数!
说明:断言仅用于调试阶段,生产环境中可通过-O参数(优化模式)关闭断言(运行时忽略assert语句),避免影响程序性能。
2.2.3 pdb调试(命令行调试,适合复杂代码)
pdb是Python内置的命令行调试工具,可实现断点设置、单步执行、查看变量值等功能,适用于代码量较大、逻辑复杂的场景。pdb调试有两种启动方式:
方式1:命令行启动(直接调试整个脚本)
在终端中执行:python -m pdb 脚本文件名.py,进入pdb调试模式。
方式2:脚本内启动(在指定位置设置断点)
在脚本中导入pdb模块,在需要调试的位置添加pdb.set_trace(),运行脚本时会自动进入调试模式。
pdb常用调试命令(必记)
|
命令 |
功能说明 |
|---|---|
|
h / help |
查看所有调试命令的帮助 |
|
n / next |
单步执行(跳过函数调用,不进入函数内部) |
|
s / step |
单步执行(进入函数内部,逐行执行) |
|
b / break 行号 |
在指定行设置断点(如b 10,在第10行设置断点) |
|
b / break 函数名 |
在指定函数的入口设置断点 |
|
c / continue |
继续执行,直到遇到下一个断点或程序结束 |
|
p / print 变量名 |
打印指定变量的值(如p a,打印变量a的值) |
|
l / list |
查看当前调试位置附近的代码(默认显示11行) |
|
q / quit |
退出pdb调试模式 |
|
r / return |
执行到当前函数结束,并返回结果 |
pdb调试实操案例
编写一个有错误的函数,用pdb调试定位错误:
import pdb
def add_list(lst):
total = 0
pdb.set_trace() # 设置断点,运行到此处进入调试模式
for num in lst:
total += num # 假设错误:应为total *= num
return total
# 调用函数,预期结果:1*2*3=6,实际结果:6(若错误则为6,此处模拟错误)
print(add_list([1, 2, 3]))
调试步骤:
-
运行脚本,自动进入pdb调试模式,提示
(pdb)。 -
输入
l,查看当前代码位置,确认断点在for循环前。 -
输入
n,单步执行到for循环,输入p total,查看total初始值为0。 -
继续输入
n,逐行执行循环,观察total的值变化:第一次循环后total=1,第二次=3,第三次=6(若逻辑错误为加法,此处会发现结果不符合预期)。 -
发现循环内的逻辑错误(应为乘法),修改代码后,退出调试(输入
q),重新运行验证。
2.2.4 IDE调试(最常用,适合项目开发)
日常开发中,最常用的调试方式是通过IDE(如PyCharm、VS Code)自带的调试工具,图形化界面操作,无需记忆命令,效率更高。以下以PyCharm为例,讲解IDE调试的核心操作。
PyCharm调试步骤
-
设置断点:在需要调试的代码行左侧,点击鼠标左键,出现红色圆点(断点),断点可设置在任意行(通常设置在异常可能发生的位置、关键逻辑处)。
-
启动调试:点击PyCharm右上角的“调试”按钮(绿色虫子图标),或右键点击脚本,选择“Debug 脚本名.py”,启动调试模式。
-
调试操作:调试模式启动后,程序会停在第一个断点处,此时可通过工具栏的按钮进行操作:
-
单步执行(F8):逐行执行代码,跳过函数调用(类似pdb的n命令)。
-
步入函数(F7):进入当前执行的函数内部,逐行调试(类似pdb的s命令)。
-
步出函数(Shift+F8):执行完当前函数的剩余代码,返回上一层调用(类似pdb的r命令)。
-
继续执行(F9):继续执行代码,直到遇到下一个断点或程序结束(类似pdb的c命令)。
-
停止调试(Ctrl+F2):终止调试模式。
-
-
查看变量:调试时,PyCharm右侧会显示“Variables”面板,实时显示当前作用域内的所有变量及其值,可直接查看、修改变量值,快速定位错误。
-
添加监视:若需要重点关注某个变量,可在“Watches”面板中添加变量,实时监视其值的变化(右键选择“Add Watch”)。
IDE调试的优势
图形化界面,操作简单;实时查看变量值,无需手动打印;支持断点条件设置(仅当满足某个条件时,程序才停在断点处);支持多线程、多进程调试,适合复杂项目。
2.3 调试技巧与常见问题
2.3.1 调试技巧
-
断点精准设置:不要在每一行都设置断点,重点设置在异常触发点、函数入口/出口、关键逻辑判断处,提高调试效率。
-
利用条件断点:在IDE中设置断点时,可添加条件(如“num == 0”),只有当条件满足时,程序才会停在断点处,适合排查特定场景下的错误。
-
分步调试+变量监视:对于复杂逻辑,采用“分步执行+实时查看变量”的方式,逐步缩小错误范围,避免盲目修改代码。
-
结合日志调试:对于无法实时调试的场景(如线上程序),可通过日志模块(logging)记录关键信息(变量值、执行流程),事后分析日志定位错误。
-
简化代码调试:若代码逻辑过于复杂,可临时简化代码(注释掉无关逻辑),聚焦于错误相关的代码,调试完成后再恢复。
2.3.2 常见调试问题及解决方法
-
断点不生效:检查是否启动了“调试模式”(而非普通运行);检查断点是否设置在可执行行(如注释行、空行无法设置有效断点);检查IDE是否开启了“跳过断点”模式。
-
变量值无法查看:检查变量是否在当前作用域内(如函数内的局部变量,在函数外无法查看);检查调试是否停在变量定义之后(若变量未定义,无法查看其值)。
-
程序调试时卡顿:可能是代码中存在死循环,可通过“暂停调试”(Ctrl+Pause),查看当前执行的代码行,定位死循环位置。
-
线上程序无法调试:线上程序不可直接调试,可通过添加日志、捕获异常并记录详细信息(如异常堆栈),线下复现问题后再调试。
2.4 日志调试(适合线上程序)
对于线上运行的程序,无法实时调试,此时可通过Python的logging模块记录日志,包含变量值、执行流程、异常信息等,事后通过日志分析错误原因。
logging模块的核心优势:可设置日志级别(DEBUG、INFO、WARNING、ERROR、CRITICAL),灵活控制日志输出;可将日志写入文件,便于长期保存和分析;支持格式化日志信息,包含时间、日志级别、代码位置等。
logging调试实操案例
import logging
# 配置日志:设置日志级别、日志格式、输出方式(控制台+文件)
logging.basicConfig(
level=logging.DEBUG, # 日志级别:DEBUG(最低级别,记录所有日志)
format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s", # 日志格式
handlers=[
logging.FileHandler("debug.log"), # 日志写入文件
logging.StreamHandler() # 日志输出到控制台
]
)
def calculate(a, b):
logging.debug(f"开始计算,输入参数:a={a}, b={b}") # 调试日志
try:
result = a / b
logging.info(f"计算成功,结果:{result}") # 信息日志
return result
except ZeroDivisionError as e:
logging.error(f"计算失败:{e}", exc_info=True) # 错误日志,exc_info=True记录异常堆栈
raise
# 调用函数
calculate(10, 0)
运行后,会生成debug.log文件,同时控制台输出日志,日志中包含异常堆栈信息,可清晰看到错误发生的代码行和原因,便于事后排查。
第三部分:综合实操案例
结合异常处理和程序调试,完成一个“文件读写”的综合案例,模拟实际开发中的问题及解决过程。
案例需求
编写一个Python程序,实现以下功能:
-
读取指定路径的文本文件(文件内容为数字,每行一个数字)。
-
计算文件中所有数字的总和。
-
将计算结果写入一个新的文本文件中。
-
妥善处理可能出现的异常(文件未找到、文件内容不是数字、权限不足等)。
-
若出现异常,通过调试定位并解决问题。
步骤1:编写初始代码(存在异常)
def read_and_calculate(file_path):
# 读取文件
f = open(file_path, "r", encoding="utf-8")
content = f.readlines()
f.close()
# 计算总和
total = 0
for line in content:
num = int(line) # 若行内容不是数字,会触发ValueError
total += num
# 写入结果
with open("result.txt", "w", encoding="utf-8") as f:
f.write(f"所有数字的总和:{total}")
return total
# 调用函数
read_and_calculate("numbers.txt")
步骤2:运行代码,触发异常并调试
假设当前目录不存在numbers.txt文件,运行代码会触发FileNotFoundError,程序崩溃。
调试过程:
-
通过异常信息,定位错误位置:
open(file_path, "r")行,提示文件未找到。 -
分析原因:文件路径错误,或文件不存在。
-
修改方案:添加异常捕获,提示用户检查文件路径;同时,使用finally块或with语句确保文件资源释放。
步骤3:优化代码(添加异常处理)
def read_and_calculate(file_path):
try:
# 读取文件(使用with语句自动释放资源)
with open(file_path, "r", encoding="utf-8") as f:
content = f.readlines()
# 计算总和
total = 0
for idx, line in enumerate(content, 1):
try:
num = int(line.strip()) # 去除换行符,避免空格导致的错误
total += num
except ValueError:
print(f"警告:第{idx}行内容不是有效数字,已跳过(内容:{line.strip()})")
# 写入结果
with open("result.txt", "w", encoding="utf-8") as f:
f.write(f"所有数字的总和:{total}")
print(f"计算完成,总和已写入result.txt文件,总和为:{total}")
return total
except FileNotFoundError:
print(f"错误:文件 {file_path} 未找到,请检查文件路径是否正确!")
except PermissionError:
print(f"错误:权限不足,无法读取文件 {file_path} 或写入result.txt文件!")
except Exception as e:
print(f"发生未知错误:{e}")
# 记录异常堆栈,便于调试
import traceback
traceback.print_exc()
# 调用函数
read_and_calculate("numbers.txt")
步骤4:再次调试,验证优化效果
-
场景1:numbers.txt不存在 → 触发FileNotFoundError,提示用户检查路径,程序不崩溃。
-
场景2:numbers.txt存在,但部分行不是数字 → 触发ValueError,提示用户跳过无效行,继续计算,程序正常运行。
-
场景3:权限不足 → 触发PermissionError,提示用户权限问题,程序不崩溃。
通过异常处理,程序的健壮性大幅提升;通过调试,定位并解决了初始代码中的资源释放、异常未处理等问题。
总结
Python异常处理的核心是“预防崩溃、优雅处理”,通过try-except语句捕获异常,结合else、finally、raise等语法,实现对异常的精准控制;自定义异常则能适配业务场景,提升代码可读性。
程序调试的核心是“定位错误、解决错误”,从基础的print打印、断言,到专业的pdb命令行调试、IDE图形化调试,再到线上的日志调试,需根据场景选择合适的方式。调试的关键是“耐心、细致”,通过逐步排查,找到错误的根源,避免盲目修改代码。
掌握异常处理与程序调试,能有效提升代码的健壮性和可维护性,减少程序崩溃的概率,是每个Python开发者必备的技能。建议多动手实操,结合案例练习,逐步熟练运用各类异常处理语法和调试工具。
更多推荐



所有评论(0)