目录

前言

第一部分:Python异常处理

1.1 异常的概念与常见类型

1.1.1 什么是异常

1.1.2 常见异常类型及场景

1.2 异常处理的核心语法

1.2.1 基本语法(try-except)

1.2.2 捕获多个异常

方式1:多个except子句(推荐,针对性处理)

方式2:单个except捕获多个异常(用元组包裹)

1.2.3 try-except-else语句

1.2.4 try-except-finally语句

1.2.5 raise语句(主动抛出异常)

1.3 自定义异常

1.3.1 自定义异常的基本写法

1.3.2 自定义异常的应用场景

1.4 异常处理的最佳实践

第二部分:Python程序调试

2.1 调试的核心思路

2.2 常见调试方式(从简单到复杂)

2.2.1 print打印调试(最基础,适合简单代码)

2.2.2 断言(assert)调试(适合逻辑校验)

2.2.3 pdb调试(命令行调试,适合复杂代码)

方式1:命令行启动(直接调试整个脚本)

方式2:脚本内启动(在指定位置设置断点)

pdb常用调试命令(必记)

pdb调试实操案例

2.2.4 IDE调试(最常用,适合项目开发)

PyCharm调试步骤

IDE调试的优势

2.3 调试技巧与常见问题

2.3.1 调试技巧

2.3.2 常见调试问题及解决方法

2.4 日志调试(适合线上程序)

logging调试实操案例

第三部分:综合实操案例

案例需求

步骤1:编写初始代码(存在异常)

步骤2:运行代码,触发异常并调试

步骤3:优化代码(添加异常处理)

步骤4:再次调试,验证优化效果

总结


前言

在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 调试的核心思路

无论使用哪种调试方式,核心思路都是一致的:

  1. 定位错误:确定错误发生的代码行、错误类型(语法错误/异常)。

  2. 分析原因:结合错误信息,分析代码逻辑、变量值、输入输出是否符合预期。

  3. 修改错误:根据原因修改代码,避免引入新的错误。

  4. 验证结果:运行代码,确认错误已解决,且程序运行符合预期。

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]))

调试步骤:

  1. 运行脚本,自动进入pdb调试模式,提示(pdb)

  2. 输入l,查看当前代码位置,确认断点在for循环前。

  3. 输入n,单步执行到for循环,输入p total,查看total初始值为0。

  4. 继续输入n,逐行执行循环,观察total的值变化:第一次循环后total=1,第二次=3,第三次=6(若逻辑错误为加法,此处会发现结果不符合预期)。

  5. 发现循环内的逻辑错误(应为乘法),修改代码后,退出调试(输入q),重新运行验证。

2.2.4 IDE调试(最常用,适合项目开发)

日常开发中,最常用的调试方式是通过IDE(如PyCharm、VS Code)自带的调试工具,图形化界面操作,无需记忆命令,效率更高。以下以PyCharm为例,讲解IDE调试的核心操作。

PyCharm调试步骤
  1. 设置断点:在需要调试的代码行左侧,点击鼠标左键,出现红色圆点(断点),断点可设置在任意行(通常设置在异常可能发生的位置、关键逻辑处)。

  2. 启动调试:点击PyCharm右上角的“调试”按钮(绿色虫子图标),或右键点击脚本,选择“Debug 脚本名.py”,启动调试模式。

  3. 调试操作:调试模式启动后,程序会停在第一个断点处,此时可通过工具栏的按钮进行操作:

    1. 单步执行(F8):逐行执行代码,跳过函数调用(类似pdb的n命令)。

    2. 步入函数(F7):进入当前执行的函数内部,逐行调试(类似pdb的s命令)。

    3. 步出函数(Shift+F8):执行完当前函数的剩余代码,返回上一层调用(类似pdb的r命令)。

    4. 继续执行(F9):继续执行代码,直到遇到下一个断点或程序结束(类似pdb的c命令)。

    5. 停止调试(Ctrl+F2):终止调试模式。

  4. 查看变量:调试时,PyCharm右侧会显示“Variables”面板,实时显示当前作用域内的所有变量及其值,可直接查看、修改变量值,快速定位错误。

  5. 添加监视:若需要重点关注某个变量,可在“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. 读取指定路径的文本文件(文件内容为数字,每行一个数字)。

  2. 计算文件中所有数字的总和。

  3. 将计算结果写入一个新的文本文件中。

  4. 妥善处理可能出现的异常(文件未找到、文件内容不是数字、权限不足等)。

  5. 若出现异常,通过调试定位并解决问题。

步骤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,程序崩溃。

调试过程:

  1. 通过异常信息,定位错误位置:open(file_path, "r") 行,提示文件未找到。

  2. 分析原因:文件路径错误,或文件不存在。

  3. 修改方案:添加异常捕获,提示用户检查文件路径;同时,使用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. 场景1:numbers.txt不存在 → 触发FileNotFoundError,提示用户检查路径,程序不崩溃。

  2. 场景2:numbers.txt存在,但部分行不是数字 → 触发ValueError,提示用户跳过无效行,继续计算,程序正常运行。

  3. 场景3:权限不足 → 触发PermissionError,提示用户权限问题,程序不崩溃。

通过异常处理,程序的健壮性大幅提升;通过调试,定位并解决了初始代码中的资源释放、异常未处理等问题。

总结

Python异常处理的核心是“预防崩溃、优雅处理”,通过try-except语句捕获异常,结合else、finally、raise等语法,实现对异常的精准控制;自定义异常则能适配业务场景,提升代码可读性。

程序调试的核心是“定位错误、解决错误”,从基础的print打印、断言,到专业的pdb命令行调试、IDE图形化调试,再到线上的日志调试,需根据场景选择合适的方式。调试的关键是“耐心、细致”,通过逐步排查,找到错误的根源,避免盲目修改代码。

掌握异常处理与程序调试,能有效提升代码的健壮性和可维护性,减少程序崩溃的概率,是每个Python开发者必备的技能。建议多动手实操,结合案例练习,逐步熟练运用各类异常处理语法和调试工具。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐