跳到主要内容

一个分布式系统就是这样的一种系统:你甚至不知道存在的一台电脑的故障,能把你自己电脑的故障搞出来。

Leslie Lamport分布式系统先驱

🔍 调试进阶

📌 本节要点

  • logging 模块替代 print,实现分级日志与持久化
  • 掌握条件断点和高级 pdb 调试技巧
  • 配置 debugpy 实现远程调试和 VS Code attach
  • 调试异步代码的常见陷阱
  • 识别 Python 经典 bug 模式,防患于未然

1. logging 模块

1.1 为什么 logging 比 print 好

print 适合调试时快速查看,但生产环境需要更专业的方案:

对比项printlogging
级别控制DEBUG/INFO/WARNING/ERROR/CRITICAL
持久化手动重定向内置 FileHandler
格式化字符串拼接结构化格式
模块区分困难Logger 名称自动区分

1.2 基本配置

Python
import logging

# 基本配置
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)

logging.debug('调试信息')
logging.info('普通信息')
logging.warning('警告信息')
logging.error('错误信息')
logging.critical('严重错误')

1.3 日志级别

从低到高排列,级别越低输出越多:

DEBUG(10) → INFO(20) → WARNING(30) → ERROR(40) → CRITICAL(50)

1.4 Logger / Handler / Formatter 架构

Python
import logging

# 创建 logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)

# Console handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# File handler
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.DEBUG)

# Formatter
fmt = logging.Formatter('%(asctime)s [%(name)s] %(levelname)s: %(message)s')
console.setFormatter(fmt)
file_handler.setFormatter(fmt)

# 添加 handlers
logger.addHandler(console)
logger.addHandler(file_handler)

# 使用
logger.debug('这条只写入文件')
logger.info('这条控制台和文件都有')

1.5 字典配置

Python
import logging.config

config = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'detailed',
},
'file': {
'class': 'logging.FileHandler',
'filename': 'app.log',
'level': 'DEBUG',
'formatter': 'detailed',
},
},
'formatters': {
'detailed': {
'format': '%(asctime)s %(name)s %(levelname)s: %(message)s'
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file'],
},
}

logging.config.dictConfig(config)
logging.info('字典配置生效')
logging 配置示例

2. 条件断点和高级 pdb

2.1 pdb 基本用法

Python
import pdb

def calculate(n):
result = 0
for i in range(n):
pdb.set_trace() # 在此处暂停
result += i
return result

calculate(5)

2.2 条件断点

Python
import pdb

def process_items(items):
for idx, item in enumerate(items):
# 只在 item 为 None 时暂停
if item is None:
pdb.set_trace()
print(f'Processing: {item}')

process_items(['a', None, 'b', None, 'c'])

2.3 常用 pdb 命令

命令说明
n (next)执行下一行
s (step)进入函数内部
c (continue)继续执行到下一个断点
p expr打印表达式值
l (list)显示当前代码
w (where)显示调用栈
q (quit)退出调试器

2.4 ipdb 改进版

Shell
pip install ipdb
Python
import ipdb

def buggy_function(data):
result = []
for item in data:
ipdb.set_trace() # 语法高亮、自动补全
result.append(item * 2)
return result

3. 远程调试

3.1 debugpy 安装

Shell
pip install debugpy

3.2 服务端启动

Python
import debugpy

# 监听 5678 端口,等待 VS Code 连接
debugpy.listen(('0.0.0.0', 5678))
print('等待调试器连接...')
debugpy.wait_for_client() # 可选:阻塞等待
debugpy.breakpoint() # 可选:自动断点

# 你的应用代码
def main():
x = 10
y = 20
print(x + y)

if __name__ == '__main__':
main()

3.3 VS Code launch.json 配置

launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
}
]
}

3.4 SSH 远程调试流程

Shell
# 1. 在远程机器启动 debugpy
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client your_script.py

# 2. SSH 端口转发
ssh -L 5678:localhost:5678 user@remote-server

# 3. VS Code Attach 到 localhost:5678

4. 调试异步代码

4.1 开启 asyncio debug 模式

Python
import asyncio

async def fetch_data():
await asyncio.sleep(1)
return {'data': 'value'}

async def main():
result = await fetch_data()
print(result)

# debug=True 检测未 await 的协程和慢回调
asyncio.run(main(), debug=True)

4.2 常见异步 bug

Python
import asyncio

# BUG 1: 忘记 await
async def bad_example():
asyncio.sleep(1) # 缺少 await,返回协程对象而非结果

# BUG 2: 阻塞调用
async def also_bad():
import time
time.sleep(1) # 阻塞整个事件循环!

# BUG 3: 任务未取消
async def leaked_task():
while True:
await asyncio.sleep(1)

async def main():
task = asyncio.create_task(leaked_task())
# 忘记取消 task,程序不会退出

4.3 调试技巧

Python
import asyncio

async def worker(name):
await asyncio.sleep(1)
return f'{name} done'

async def main():
tasks = [worker(f'task-{i}') for i in range(3)]
results = await asyncio.gather(*tasks)
print(results)

# 查看所有运行中的任务
all_tasks = asyncio.all_tasks()
print(f'运行中的任务数: {len(all_tasks)}')

asyncio.run(main())
异步任务追踪

5. 经典 bug 模式

5.1 可变默认参数

Python
# 经典陷阱
def append_to(element, target=[]):
target.append(element)
return target

print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← 期望 [2]!

# 正确写法
def append_to_fixed(element, target=None):
if target is None:
target = []
target.append(element)
return target

print(append_to_fixed(1)) # [1]
print(append_to_fixed(2)) # [2] ← 正确

5.2 闭包中的循环变量

Python
# 经典陷阱
functions = [lambda x: x * i for i in range(5)]
print([f(10) for f in functions]) # [40, 40, 40, 40, 40]

# 正确写法:用默认参数捕获当前值
functions = [lambda x, i=i: x * i for i in range(5)]
print([f(10) for f in functions]) # [0, 10, 20, 30, 40]

5.3 浮点数精度

Python
# 经典陷阱
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False

# 正确写法
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True

from decimal import Decimal
a = Decimal('0.1') + Decimal('0.2')
print(a == Decimal('0.3')) # True

5.4 隐式字符串拼接

Python
# 意外拼接
result = (
"第一部分"
"第二部分"
)
print(result) # 第一部分第二部分

# 用于分隔长字符串是好的,但要小心
query = (
"SELECT * FROM users "
"WHERE name = 'test'"
)

5.5 is vs ==

Python
# == 比较值
# is 比较身份(内存地址)

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True
print(a is b) # False

# 小整数缓存(-5 到 256)
x = 256
y = 256
print(x is y) # True(CPython 优化)

x = 257
y = 257
print(x is y) # False(可能)

# 正确用法
print(a is None) # 推荐:检查 None
print(a == None) # 不推荐

5.6 列表推导式变量泄漏

Python
# Python 2 问题(Python 3 已修复)
# x 会泄漏到外部作用域

# Python 3 中列表推导式有自己的作用域
[print(i) for i in range(5)] # 输出 0-4
try:
print(i) # NameError: i 未定义
except NameError:
print('i 在列表推导式外不可见')

# 但生成器表达式也泄漏变量(Python 2)
# 保持良好习惯,不要依赖此行为

6. 实战:调试有多个隐蔽 bug 的程序

调试实战

修复后的代码

Python
import math

# 修复 Bug 1
def process_data(items, cache=None):
if cache is None:
cache = []
for item in items:
cache.append(item * 2)
return cache

# 修复 Bug 2
def create_multipliers():
return [lambda x, i=i: x * i for i in range(4)]

# 修复 Bug 3
def calculate_average(numbers):
total = sum(numbers)
avg = total / len(numbers)
return avg

# 验证
data1 = [1, 2, 3]
data2 = [4, 5, 6]
print(process_data(data1)) # [2, 4, 6]
print(process_data(data2)) # [8, 10, 12] ← 正确隔离

multipliers = create_multipliers()
print([m(10) for m in multipliers]) # [0, 10, 20, 30]

avg = calculate_average([0.1, 0.2])
print(math.isclose(avg, 0.15)) # True

🎯 动手练习

  1. logging 练习:为一个模拟 Web 服务器配置日志,INFO 级别输出到控制台,DEBUG 级别写入文件
  2. pdb 练习:用条件断点调试一个数据处理函数,只在数据为负数时暂停
  3. bug 修复:修复以下代码中的可变默认参数和闭包陷阱
Python
def count_items(items, counter={}):
for item in items:
counter[item] = counter.get(item, 0) + 1
return counter

# 找出问题并修复

📚 延伸阅读


📊 速查表

logging 级别速查

级别数值使用场景
DEBUG10开发调试,详细信息
INFO20确认程序按预期运行
WARNING30潜在问题,但程序仍可运行
ERROR40严重问题,功能无法执行
CRITICAL50程序可能无法继续运行

pdb 命令速查

命令缩写说明
nextn执行下一行(不进入函数)
steps进入函数内部
continuec继续执行到下一断点
printp打印表达式值
listl显示当前代码上下文
wherew显示调用栈
quitq退出调试器
breakb设置断点
clearcl清除断点

常见异常速查表

异常原因解决方案
NameError变量未定义检查拼写和作用域
TypeError类型不匹配检查参数类型
ValueError值无效检查输入值范围
IndexError索引越界检查列表长度
KeyError字典键不存在.get() 或检查键
AttributeError对象无此属性检查对象类型和方法名
ImportError模块导入失败检查模块名和安装

✅ 本节总结

  • logging 模块:替代 print,实现分级、持久化、格式化日志
  • 高级 pdb:条件断点和 ipdb 提升调试效率
  • 远程调试:debugpy + VS Code 实现远程 attach
  • 异步调试:开启 debug 模式,注意 await 和任务管理
  • 经典 bug 模式:可变默认参数、闭包陷阱、浮点精度等陷阱要牢记

掌握这些调试技巧,便能高效定位和修复各种隐蔽的 bug!