Python 调试技巧
调试是开发过程中不可或缺的技能。本章将介绍 Python 中各种调试方法和工具,帮助你快速定位和解决问题。
调试基础
为什么调试重要?
- 快速定位问题:减少排查时间
- 理解代码逻辑:深入理解程序执行流程
- 预防潜在 bug:提前发现隐藏的问题
- 提高代码质量:通过调试过程改进代码设计
调试流程
print 调试法
最简单直接的调试方式。
基本使用
def calculate_sum(numbers):
print(f"输入: {numbers}") # 打印输入
total = 0
for i, num in enumerate(numbers):
total += num
print(f"第 {i} 次循环: num={num}, total={total}") # 打印中间结果
print(f"结果: {total}") # 打印输出
return total
calculate_sum([1, 2, 3, 4, 5])
使用 f-string 调试
def debug_variables():
x = 10
y = 20
z = "hello"
# 使用 f-string 打印变量
print(f"{x=}, {y=}, {z=}")
print(f"{x + y = }")
print(f"{len(z) = }")
debug_variables()
调试装饰器
import functools
import time
def debug_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} 返回: {result}")
return result
return wrapper
@debug_decorator
def add(a, b):
return a + b
add(3, 5)
解释:装饰器可以批量添加调试代码,无需修改原函数。
assert 断言调试
使用断言进行运行时检查。
基本用法
def divide(a, b):
assert b != 0, "除数不能为零" # 条件为 False 时抛出 AssertionError
return a / b
result = divide(10, 0) # AssertionError: 除数不能为零
断言使用场景
def process_data(data):
# 检查输入
assert isinstance(data, list), f"期望列表,实际得到 {type(data)}"
assert len(data) > 0, "数据不能为空"
# 检查数据范围
for i, item in enumerate(data):
assert item >= 0, f"第 {i} 个元素为负数: {item}"
# 检查输出
result = sum(data)
assert result >= 0, f"结果不应该为负数: {result}"
return result
process_data([1, 2, 3, -4])
关闭断言
python -O script.py # 优化模式,assert 语句被忽略
解释:python -O 会移除所有 assert 语句,用于生产环境提高性能。
traceback 模块
获取详细的异常信息。
基本使用
import traceback
def function_c():
raise ValueError("发生错误")
def function_b():
function_c()
def function_a():
function_b()
try:
function_a()
except Exception as e:
print("捕获到异常:")
traceback.print_exc()
输出:
捕获到异常:
Traceback (most recent call last):
File "script.py", line 13, in <module>
function_a()
File "script.py", line 11, in function_a
function_b()
File "script.py", line 9, in function_b
function_c()
File "script.py", line 7, in function_c
raise ValueError("发生错误")
ValueError: 发生错误
格式化异常信息
import traceback
import sys
try:
risky_function()
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
# 格式化为字符串
formatted = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
print(formatted)
# 简化的栈信息
stack_summary = traceback.extract_tb(exc_traceback)
for frame in stack_summary:
print(f"文件: {frame.filename}, 行: {frame.lineno}, 函数: {frame.name}")
自定义异常类
class ValidationError(Exception):
"""验证错误异常"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
def validate_user(user):
if not user.get("name"):
raise ValidationError("name", "用户名不能为空")
if not user.get("email"):
raise ValidationError("email", "邮箱不能为空")
if user.get("age", 0) < 0:
raise ValidationError("age", "年龄不能为负")
return True
try:
validate_user({"name": "", "age": -5})
except ValidationError as e:
print(f"验证失败: {e.field} - {e.message}")
pdb 调试器
Python 内置的交互式调试器。
基本使用
import pdb
def calculate(numbers):
total = 0
pdb.set_trace() # 设置断点
for num in numbers:
total += num
return total
calculate([1, 2, 3, 4, 5])
pdb 命令
| 命令 | 简写 | 说明 |
|---|---|---|
continue | c | 继续执行到下一个断点 |
step | s | 单步执行,进入函数 |
next | n | 单步执行,跳过函数 |
break | b | 设置断点 |
list | l | 查看当前代码上下文 |
print | p | 打印变量值 |
quit | q | 退出调试器 |
where | w | 查看调用栈 |
交互式调试示例
import pdb
def fibonacci(n):
if n <= 1:
return n
pdb.set_trace() # 在这里可以检查 n 的值
return fibonacci(n - 1) + fibonacci(n - 2)
# 常用调试命令:
# n (next) - 执行下一行
# s (step) - 进入函数
# p n - 打印变量 n
# w (where) - 查看调用栈
# u (up) - 移到上层栈
# d (down) - 移到下层栈
使用 breakpoint() (Python 3.7+)
def process_data(data):
breakpoint() # 相当于 import pdb; pdb.set_trace()
result = [x * 2 for x in data]
return result
process_data([1, 2, 3])
可以通过环境变量配置调试器:
export PYTHONBREAKPOINT=pdb.set_trace
python script.py
IDE 调试
VS Code 调试配置
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: 当前文件",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Python: 远程调试",
"type": "python",
"request": "launch",
"program": "${file}",
"connect": {
"host": "localhost",
"port": 5678
}
}
]
}
PyCharm 调试
- 在代码行号左侧点击设置断点
- 右键选择 "Debug" 或按
Shift + F9 - 使用调试工具栏:
- ▶️ 继续执行
- ⏭️ 跳过
- ⏺️ 进入
- ⏹️ 停止
断点类型
# 条件断点
# 在 PyCharm/VS Code 中右键断点设置条件
for i in range(100):
if i == 50: # 只在 i == 50 时中断
print(i)
# 日志断点
# 打印信息但不暂停
print(f"处理第 {i} 项")
# 异常断点
# 在 IDE 中设置,只在特定异常时中断
raise ValueError("调试这个异常")
日志调试
使用 logging 模块进行结构化日志记录。
基本配置
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")
分层日志记录
import logging
# 配置根日志记录器
logging.basicConfig(
level=logging.DEBUG,
format='%(levelname)s - %(name)s - %(message)s'
)
# 模块级日志记录器
logger = logging.getLogger(__name__)
class Calculator:
def __init__(self):
self.logger = logging.getLogger(f"{__name__}.Calculator")
def divide(self, a, b):
self.logger.debug(f"执行除法: {a} / {b}")
if b == 0:
self.logger.error("除数为零")
raise ValueError("除数不能为零")
result = a / b
self.logger.info(f"结果: {result}")
return result
calc = Calculator()
calc.divide(10, 2)
日志级别
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 只记录 WARNING 及以上级别
logger.setLevel(logging.WARNING)
logger.debug("调试") # 不输出
logger.info("信息") # 不输出
logger.warning("警告") # 输出
logger.error("错误") # 输出
日志到文件
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("这条日志会同时输出到文件和控制台")
调试时临时开启详细日志
import logging
import sys
def enable_debug_logging():
"""临时开启调试模式"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
)
# 设置所有日志记录器为 DEBUG 级别
for name in logging.Logger.manager.loggerDict:
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 仅在命令行指定 --debug 时开启
if '--debug' in sys.argv:
enable_debug_logging()
单元测试中的调试
使用 pytest 调试
import pytest
def test_example():
x = 10
y = 20
# 使用 pytest.set_trace() 断点 (需要 pytest-pdb)
# pytest.set_trace()
assert x + y == 30
# 运行并进入调试
# pytest --pdb script.py
# 运行到第一个失败后停止
# pytest -x
# 显示局部变量
# pytest --showlocals -v
使用 unittest 调试
import unittest
class TestMath(unittest.TestCase):
def test_division(self):
# 失败时打印信息
result = 10 / 2
self.assertEqual(result, 5, f"期望 5,实际得到 {result}")
# 使用 addCleanup 清理
self.addCleanup(lambda: print("测试后清理"))
if __name__ == '__main__':
unittest.main(verbosity=2)
远程调试
使用 remote-pdb
pip install remote-pdb
from remote_pdb import remote_pdb
def some_function():
# 远程调试
with remote_pdb.RemotePdb('127.0.0.1', 4444):
# 在这里可以连接调试
x = calculate_something()
print(x)
some_function()
连接调试器:
telnet 127.0.0.1 4444
# 或使用 nc
nc 127.0.0.1 4444
使用 debugpy (VS Code remote debugging)
pip install debugpy
import debugpy
debugpy.listen(("0.0.0.0", 5678))
print("等待调试器连接...")
# 暂停等待连接
debugpy.wait_for_client()
# 你的代码
x = calculate_something()
常见错误及调试方法
1. 变量未定义
# NameError: name 'x' is not defined
print(x) # x 从未被定义
# 调试方法
# 1. 检查变量是否在作用域内
# 2. 检查是否拼写错误
# 3. 检查变量是否在 if 条件分支内定义
2. 类型错误
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
result = 10 + "5"
# 调试方法
print(type(10), type("5")) # 检查类型
result = 10 + int("5") # 类型转换
3. 索引错误
# IndexError: list index out of range
lst = [1, 2, 3]
print(lst[5])
# 调试方法
print(len(lst)) # 检查列表长度
print(lst[-1]) # 使用负索引从后往前
4. 逻辑错误(最难调试)
# 结果不正确但没有报错
def calculate_average(scores):
total = sum(scores)
# 错误:忘记除以数量
return total
# 调试方法
print(f"总分: {total}, 人数: {len(scores)}")
return total / len(scores)
5. 递归深度错误
# RecursionError: maximum recursion depth exceeded
def factorial(n):
return n * factorial(n - 1) # 缺少基线条件
# 调试方法
import sys
sys.setrecursionlimit(2000) # 增加限制
def factorial(n):
if n <= 1:
return 1 # 添加基线条件
return n * factorial(n - 1)
调试技巧总结
| 场景 | 推荐方法 |
|---|---|
| 快速定位问题 | print 调试 |
| 运行时检查 | assert 断言 |
| 复杂问题 | pdb 断点 |
| 生产环境 | logging 日志 |
| IDE 环境 | IDE 内置调试器 |
| 远程调试 | debugpy/remote-pdb |
小结
本章我们学习了:
- print 调试法的使用技巧
- assert 断言进行运行时检查
- traceback 模块获取异常信息
- pdb 交互式调试器
- IDE 调试工具
- logging 日志系统
- 远程调试方法
- 常见错误及调试策略
练习
- 使用 pdb 调试一个递归函数
- 为你之前写的代码添加日志记录
- 在 VS Code 或 PyCharm 中设置条件断点
- 使用 logging 模块实现分层日志记录