跳到主要内容

Python 调试技巧

调试是开发过程中不可或缺的技能。本章将介绍 Python 中各种调试方法和工具,帮助你快速定位和解决问题。

调试基础

为什么调试重要?

  1. 快速定位问题:减少排查时间
  2. 理解代码逻辑:深入理解程序执行流程
  3. 预防潜在 bug:提前发现隐藏的问题
  4. 提高代码质量:通过调试过程改进代码设计

调试流程

最简单直接的调试方式。

基本使用

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 命令

命令简写说明
continuec继续执行到下一个断点
steps单步执行,进入函数
nextn单步执行,跳过函数
breakb设置断点
listl查看当前代码上下文
printp打印变量值
quitq退出调试器
wherew查看调用栈

交互式调试示例

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 调试

  1. 在代码行号左侧点击设置断点
  2. 右键选择 "Debug" 或按 Shift + F9
  3. 使用调试工具栏:
    • ▶️ 继续执行
    • ⏭️ 跳过
    • ⏺️ 进入
    • ⏹️ 停止

断点类型

# 条件断点
# 在 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

小结

本章我们学习了:

  1. print 调试法的使用技巧
  2. assert 断言进行运行时检查
  3. traceback 模块获取异常信息
  4. pdb 交互式调试器
  5. IDE 调试工具
  6. logging 日志系统
  7. 远程调试方法
  8. 常见错误及调试策略

练习

  1. 使用 pdb 调试一个递归函数
  2. 为你之前写的代码添加日志记录
  3. 在 VS Code 或 PyCharm 中设置条件断点
  4. 使用 logging 模块实现分层日志记录

延伸阅读