第10章: 错误处理与调试 — Pythonic的错误哲学
Java/Kotlin 开发者习惯了 checked exception 的"契约式"错误处理——方法签名声明可能抛出的异常,编译器强制调用方处理。Python 完全抛弃了这套体系:没有 checked exception,没有 throws 子句,没有编译期强制。取而代之的是 EAFP 哲学(Easier to Ask Forgiveness than Permission)和一套精简但强大的异常机制。本章从异常层次出发,覆盖 EAFP 哲学、异常链、traceback 分析、调试器使用,以及 3.11+ 的错误提示革命。
10.1 异常层次: BaseException → Exception → 具体异常
Java/Kotlin 对比
Throwable
├── Error
│ └── VirtualMachineError
├── Exception
│ ├── RuntimeException
│ │ ├── NullPointerException
│ │ ├── IllegalArgumentException
│ │ └── IndexOutOfBoundsException
│ │ ├── ArrayIndexOutOfBoundsException
│ │ └── StringIndexOutOfBoundsException
│ ├── IOException
│ │ ├── FileNotFoundException
│ │ └── EOFException
│ └── SQLException
└── ...
public void readFile(String path) throws IOException { ... }
public int divide(int a, int b) { return a / b; }
fun readFile(path: String): String {
return File(path).readText()
}
Python 实现
try:
int("not_a_number")
except ValueError as e:
print(f"ValueError: {e}")
try:
"hello" + 42
except TypeError as e:
print(f"TypeError: {e}")
try:
d = {"a": 1}
d["b"]
except KeyError as e:
print(f"KeyError: {e}")
try:
[1, 2, 3][10]
except IndexError as e:
print(f"IndexError: {e}")
try:
(42).append(1)
except AttributeError as e:
print(f"AttributeError: {e}")
try:
open("/nonexistent/file.txt")
except FileNotFoundError as e:
print(f"FileNotFoundError: {e}")
it = iter([1, 2])
next(it)
next(it)
try:
next(it)
except StopIteration:
print("迭代器已耗尽")
class ValidationError(Exception):
"""业务验证失败"""
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NotFoundError(ValidationError):
"""资源不存在"""
pass
class PermissionDeniedError(ValidationError):
"""权限不足"""
pass
def get_user(user_id: int) -> dict:
if user_id < 0:
raise ValidationError("user_id", "must be positive")
if user_id == 0:
raise NotFoundError("user_id", "user not found")
return {"id": user_id, "name": "Alice"}
try:
get_user(0)
except NotFoundError as e:
print(f"未找到: {e}")
print(f"字段: {e.field}")
except ValidationError as e:
print(f"验证失败: {e}")
print(issubclass(NotFoundError, ValidationError))
print(issubclass(NotFoundError, Exception))
print(issubclass(NotFoundError, ValueError))
print(isinstance(NotFoundError("x", "y"), Exception))
核心差异
| 维度 | Java | Kotlin | Python |
|---|
| 异常根类 | Throwable | Throwable(JVM) | BaseException |
| "普通"异常基类 | Exception | Exception | Exception |
| checked exception | 有(编译器强制) | 无 | 无 |
| 系统级异常 | Error | Error | SystemExit, KeyboardInterrupt |
| 自定义异常基类 | 继承 Exception 或 RuntimeException | 同 Java | 继承 Exception |
| 方法签名声明 | throws IOException | 无 throws | 无 throws |
常见陷阱
class MyError(BaseException):
pass
class MyError(Exception):
pass
try:
get_user(0)
except ValidationError as e:
print(f"验证失败: {e}")
except NotFoundError as e:
print(f"未找到: {e}")
try:
get_user(0)
except NotFoundError as e:
print(f"未找到: {e}")
except ValidationError as e:
print(f"验证失败: {e}")
try:
...
except:
pass
try:
...
except Exception:
pass
自定义异常类设计最佳实践
Java/Kotlin 对比
public class AppException extends RuntimeException {
private final ErrorCode code;
public AppException(ErrorCode code, String message) {
super(message);
this.code = code;
}
}
public class UserNotFoundException extends AppException { ... }
public class PermissionDeniedException extends AppException { ... }
Python 实现
class AppError(Exception):
"""应用基础异常 — 所有自定义异常的父类"""
def __init__(self, message: str, code: str = "INTERNAL_ERROR"):
self.message = message
self.code = code
super().__init__(message)
class ValidationError(AppError):
"""输入验证失败"""
def __init__(self, field: str, message: str):
super().__init__(message, code="VALIDATION_ERROR")
self.field = field
class NotFoundError(AppError):
"""资源不存在"""
def __init__(self, resource: str, resource_id):
super().__init__(f"{resource} {resource_id} not found", code="NOT_FOUND")
self.resource = resource
self.resource_id = resource_id
class PermissionError(AppError):
"""权限不足"""
def __init__(self, action: str):
super().__init__(f"Permission denied: {action}", code="FORBIDDEN")
def get_user(user_id: int):
if user_id < 0:
raise ValidationError("user_id", "must be positive")
if user_id == 0:
raise NotFoundError("User", user_id)
return {"id": user_id, "name": "Alice"}
def handle_request(user_id: int):
try:
user = get_user(user_id)
return {"status": "ok", "data": user}
except ValidationError as e:
return {"status": "error", "code": e.code, "field": e.field}
except NotFoundError as e:
return {"status": "error", "code": e.code}
except AppError as e:
return {"status": "error", "code": e.code}
print(handle_request(-1))
print(handle_request(0))
print(handle_request(42))
何时使用
- 继承
Exception: 所有自定义业务异常
- 继承具体异常(如
ValueError): 当你的异常是某个内置异常的特化时
- 捕获
Exception: 顶层兜底处理
- 永远不要捕获
BaseException: 除非你明确要拦截系统退出信号
- 不要细分太多异常层次: Python 社区倾向于少量宽泛的异常类,而非 Java 式的异常森林
10.2 EAFP vs LBYL: 核心哲学差异
EAFP vs LBYL 的哲学对比详见 0.4 EAFP vs LBYL,本节聚焦错误处理的实践层面。
Java/Kotlin 对比
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
if (scores.containsKey("Bob")) {
int score = scores.get("Bob");
} else {
System.out.println("Bob not found");
}
scores.getOrDefault("Bob", 0);
Optional.ofNullable(scores.get("Bob"))
.orElse(0);
File file = new File("/tmp/data.txt");
if (file.exists()) {
if (file.canRead()) {
String content = Files.readString(file.toPath());
}
}
val map = mapOf("Alice" to 95)
if ("Bob" in map) {
println(map["Bob"])
}
map["Bob"] ?: 0
map.getValue("Bob")
map["Bob"]?.let { score ->
println(score)
}
Python 实现
d = {"Alice": 95}
if "Bob" in d:
score = d["Bob"]
else:
score = 0
try:
score = d["Bob"]
except KeyError:
score = 0
import os
if os.path.exists("/tmp/data.txt"):
with open("/tmp/data.txt") as f:
content = f.read()
try:
with open("/tmp/data.txt") as f:
content = f.read()
except FileNotFoundError:
content = ""
if hasattr(obj, "name"):
name = obj.name
else:
name = "default"
try:
name = obj.name
except AttributeError:
name = "default"
name = getattr(obj, "name", "default")
value = "42"
if isinstance(value, (int, float)):
result = value * 2
elif isinstance(value, str) and value.isdigit():
result = int(value) * 2
else:
result = 0
try:
result = int(value) * 2
except (ValueError, TypeError):
result = 0
items = iter([1, 2, 3])
if items is not None:
try:
first = next(items)
except StopIteration:
first = None
items = iter([1, 2, 3])
try:
first = next(items)
except StopIteration:
first = None
first = next(items, None)
import timeit
lbyl_code = """
d = {"key": "value"}
if "key" in d:
result = d["key"]
"""
eafp_code = """
d = {"key": "value"}
try:
result = d["key"]
except KeyError:
result = None
"""
lbyl_time = timeit.timeit(lbyl_code, number=1_000_000)
eafp_time = timeit.timeit(eafp_code, number=1_000_000)
print(f"LBYL: {lbyl_time:.4f}s")
print(f"EAFP: {eafp_time:.4f}s")
核心差异
| 维度 | LBYL (Java 风格) | EAFP (Python 风格) |
|---|
| 思维模式 | 先检查条件,再执行操作 | 直接执行,出错再处理 |
| 正常路径性能 | 每次都有检查开销 | 无额外开销 |
| 异常路径性能 | 无额外开销 | 异常捕获有开销 |
| 竞态条件 | 有(检查和使用之间可能状态变化) | 无(原子操作) |
| 代码量 | 更多(if/else 嵌套) | 更少(try/except 更扁平) |
| 适用场景 | 异常频率高时 | 异常频率低时(正常情况) |
常见陷阱
def sum_mixed_eafp(items):
total = 0
for item in items:
try:
total += item
except TypeError:
pass
return total
def sum_mixed_lbyl(items):
total = 0
for item in items:
if isinstance(item, (int, float)):
total += item
return total
try:
result = do_something()
except Exception:
result = default_value
try:
result = int(user_input)
except (ValueError, TypeError) as e:
result = default_value
if not users:
return []
try:
first = users[0]
except IndexError:
return []
何时使用
- EAFP: 正常路径成功率高(异常是少数情况),如字典访问、文件读写、类型转换
- LBYL: 异常频率高,或检查成本极低,如空集合判断、类型检查
- 经验法则: 如果 try 块内的代码 90%+ 的时间都正常执行,用 EAFP;如果失败率超过 10%,考虑 LBYL
- 竞态条件场景: 必须用 EAFP(如文件操作、网络请求)
10.3 try/except/else/finally
Java/Kotlin 对比
try {
int result = riskyOperation();
System.out.println("成功: " + result);
} catch (IOException e) {
System.err.println("IO错误: " + e.getMessage());
} catch (Exception e) {
System.err.println("其他错误: " + e.getMessage());
} finally {
System.out.println("无论如何都执行");
}
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
}
catch (IOException | SQLException e) {
System.err.println("错误: " + e.getMessage());
}
val result: Int = try {
riskyOperation()
} catch (e: IOException) {
-1
} finally {
cleanup()
}
Python 实现
def read_config(path: str) -> dict:
"""读取配置文件,带完整的错误处理"""
try:
f = open(path)
except FileNotFoundError:
print(f"配置文件不存在: {path}")
return {}
except PermissionError:
print(f"无权限读取: {path}")
return {}
else:
content = f.read()
f.close()
return {"content": content}
finally:
print("read_config 执行完毕")
def process_data(data: list) -> float:
try:
total = sum(data)
count = len(data)
except TypeError:
print("数据包含非数值类型")
return 0.0
else:
average = total / count
return average
finally:
print("process_data 完成")
def safe_convert(value, type_name: str):
"""安全类型转换"""
try:
if type_name == "int":
return int(value)
elif type_name == "float":
return float(value)
elif type_name == "str":
return str(value)
else:
raise ValueError(f"不支持的类型: {type_name}")
except (ValueError, TypeError) as e:
print(f"转换失败 [{type(e).__name__}]: {e}")
return None
print(safe_convert("42", "int"))
print(safe_convert("abc", "int"))
print(safe_convert(None, "int"))
print(safe_convert(3.14, "str"))
def parse_user_input(input_str: str) -> dict:
try:
parts = input_str.split(",")
name = parts[0].strip()
age = int(parts[1].strip())
email = parts[2].strip()
return {"name": name, "age": age, "email": email}
except ValueError as e:
print(f"年龄格式错误: {e}")
return {"error": "invalid_age"}
except IndexError as e:
print(f"字段不足,需要 name,age,email: {e}")
return {"error": "missing_fields"}
print(parse_user_input("Alice,30,alice@example.com"))
print(parse_user_input("Alice,thirty,alice@example.com"))
print(parse_user_input("Alice,30"))
def demonstrate_exception_object():
try:
int("hello")
except ValueError as e:
print(f"args: {e.args}")
print(f"str: {str(e)}")
print(f"repr: {repr(e)}")
print(f"type: {type(e)}")
print(f"has traceback: {e.__traceback__ is not None}")
demonstrate_exception_object()
def full_demo(success: bool = True):
print("\n--- success=True ---")
try:
print(" try: 开始")
if not success:
raise ValueError("出错了")
print(" try: 成功")
except ValueError as e:
print(f" except: 捕获到 {e}")
else:
print(" else: try 没有异常时执行")
finally:
print(" finally: 无论如何都执行")
full_demo(True)
full_demo(False)
try:
with open("/tmp/test.txt", "w") as f:
f.write("hello")
except IOError as e:
print(f"写入失败: {e}")
from contextlib import contextmanager
import time
@contextmanager
def timer(name: str):
"""计时上下文管理器"""
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"[{name}] 耗时: {elapsed:.4f}s")
with timer("数据处理"):
time.sleep(0.1)
核心差异
| 维度 | Java | Kotlin | Python |
|---|
| else 子句 | 无 | 无 | 有(try 无异常时执行) |
| try 是表达式 | 否 | 是 | 否 |
| 多异常捕获 | catch (A | B e) | catch (e: A) 多个 | except (A, B) as e |
| 自动资源管理 | try-with-resources | use {} | with 语句 |
| finally 返回值覆盖 | 是(反模式) | 是 | 是(反模式) |
常见陷阱
try:
result = int("42")
except ValueError:
print("值错误")
else:
raise RuntimeError("else 中的异常")
def bad_finally():
try:
raise ValueError("出错了")
finally:
return "finally 的返回值"
result = bad_finally()
print(result)
try:
1 / 0
except ZeroDivisionError as e:
error = str(e)
try:
...
except:
pass
何时使用
else: 当 try 块只包含"可能出错"的代码,而"使用结果"的代码应该和 try 分开时
finally: 资源清理(但 Python 更推荐 with 语句)
- 多异常
except (A, B): 当多种异常的处理逻辑相同时
- 分开
except: 当不同异常需要不同处理时
with 语句: 替代 finally 做资源管理(Python 的 try-with-resources)
10.4 raise ... from ...: 异常链
Java/Kotlin 对比
try {
int result = Integer.parseInt("abc");
} catch (NumberFormatException e) {
throw new ConfigurationException("配置值格式错误", e);
}
try (Resource1 r1 = new Resource1(); Resource2 r2 = new Resource2()) {
} catch (Exception e) {
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Suppressed: " + suppressed);
}
}
try {
riskyOperation()
} catch (e: IOException) {
throw ConfigurationException("配置加载失败", e)
throw ConfigurationException("配置加载失败").initCause(e)
}
Python 实现
class ConfigurationError(Exception):
"""配置错误"""
pass
def load_config_value(key: str) -> str:
"""从环境变量加载配置值"""
import os
value = os.environ[key]
return value
def get_config(key: str) -> str:
"""获取配置,包装底层异常"""
try:
return load_config_value(key)
except KeyError as e:
raise ConfigurationError(f"缺少配置项: {key}") from e
try:
get_config("DATABASE_URL")
except ConfigurationError as e:
print(f"异常: {e}")
print(f"原因 (__cause__): {e.__cause__}")
def get_config_silent(key: str) -> str:
"""获取配置,不显示底层异常"""
try:
return load_config_value(key)
except KeyError as e:
raise ConfigurationError(f"缺少配置项: {key}") from None
try:
get_config_silent("DATABASE_URL")
except ConfigurationError as e:
print(f"异常: {e}")
print(f"__cause__: {e.__cause__}")
print(f"__context__: {e.__context__}")
def auto_chain():
try:
int("not_a_number")
except ValueError:
raise ConfigurationError("配置值格式错误")
try:
auto_chain()
except ConfigurationError as e:
print(f"异常: {e}")
print(f"__cause__: {e.__cause__}")
print(f"__context__: {e.__context__}")
def demonstrate_chain_attributes():
"""演示异常链的三种属性"""
results = []
try:
try:
raise ValueError("原始错误")
except ValueError as e:
raise RuntimeError("包装错误") from e
except RuntimeError as e:
results.append(f"from: cause={type(e.__cause__).__name__}, "
f"context={type(e.__context__).__name__}")
try:
try:
raise ValueError("原始错误")
except ValueError:
raise RuntimeError("包装错误") from None
except RuntimeError as e:
results.append(f"from None: cause={e.__cause__}, "
f"suppress={e.__suppress_context__}")
try:
try:
raise ValueError("原始错误")
except ValueError:
raise RuntimeError("包装错误")
except RuntimeError as e:
results.append(f"no from: cause={e.__cause__}, "
f"context={type(e.__context__).__name__}")
for r in results:
print(r)
demonstrate_chain_attributes()
class RepositoryError(Exception):
"""数据访问层错误"""
def __init__(self, message: str, entity: str = ""):
self.entity = entity
super().__init__(message)
class ServiceError(Exception):
"""业务逻辑层错误"""
def __init__(self, message: str, operation: str = ""):
self.operation = operation
super().__init__(message)
def query_user(user_id: int) -> dict:
try:
raise ConnectionError("数据库连接超时")
except ConnectionError as e:
raise RepositoryError(f"查询用户 {user_id} 失败", "User") from e
def get_user_profile(user_id: int) -> dict:
try:
return query_user(user_id)
except RepositoryError as e:
raise ServiceError(f"获取用户资料失败: {e}", "get_user_profile") from e
try:
get_user_profile(42)
except ServiceError as e:
print(f"顶层异常: {e}")
print(f"操作: {e.operation}")
current = e
depth = 0
while current is not None:
indent = " " * depth
print(f"{indent}-> {type(current).__name__}: {current}")
current = current.__cause__
depth += 1
核心差异
| 维度 | Java | Kotlin | Python |
|---|
| 显式异常链 | new X(msg, cause) | X(msg, cause) | raise X from cause |
| 抑制底层异常 | 无直接方式 | 无直接方式 | raise X from None |
| 自动异常链 | 无(必须显式传 cause) | 无 | 有(__context__) |
| 链属性 | getCause() | cause 属性 | __cause__, __context__ |
| 附加异常 | addSuppressed() | 无 | 无 |
| traceback 显示 | Caused by: | Caused by: | The above exception was the direct cause of: |
常见陷阱
try:
risky_operation()
except ValueError:
raise ConfigurationError("配置错误")
try:
risky_operation()
except ValueError as e:
raise ConfigurationError("配置错误") from e
raise ConfigurationError("配置错误") from None
try:
int("abc")
except ValueError:
raise ConfigurationError("配置值无效")
try:
int("abc")
except ValueError as e:
raise ConfigurationError("配置值无效") from e
何时使用
raise X from e: 当新异常是原始异常的包装/转换时(如底层异常转为业务异常)
raise X from None: 当新异常替代原始异常,不想暴露实现细节时
raise X(无 from): 仅在异常处理函数/装饰器中,且不想建立显式链时
- 经验法则: 跨层传递异常时,始终用
from 明确语义
10.5 traceback 模块: 异常追踪分析
Java/Kotlin 对比
try {
int x = 1 / 0;
} catch (ArithmeticException e) {
e.printStackTrace();
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
String stackTrace = sw.toString();
StackTraceElement[] frames = e.getStackTrace();
for (StackTraceElement frame : frames) {
System.out.println(frame.getClassName() + "." +
frame.getMethodName() + "(" +
frame.getFileName() + ":" +
frame.getLineNumber() + ")");
}
}
StackWalker.getInstance()
.walk(frames -> frames
.filter(f -> f.getClassName().startsWith("com.myapp."))
.map(Object::toString)
.collect(Collectors.toList()));
try {
riskyOperation()
} catch (e: Exception) {
e.printStackTrace()
val stackTrace = e.stackTraceToString()
e.stackTrace
.filter { it.className.startsWith("com.myapp") }
.forEach { println("${it.fileName}:${it.lineNumber}") }
}
Python 实现
import traceback
import sys
def demo_print_exc():
try:
result = 1 / 0
except ZeroDivisionError:
traceback.print_exc()
demo_print_exc()
def demo_format_exc():
try:
[1, 2, 3][10]
except IndexError:
error_str = traceback.format_exc()
print(f"捕获到异常:\n{error_str}")
import logging
logging.error("处理失败", exc_info=True)
demo_format_exc()
def demo_extract_tb():
def inner():
return int("not_a_number")
def middle():
return inner()
def outer():
return middle()
try:
outer()
except ValueError:
tb = sys.exc_info()[2]
frames = traceback.extract_tb(tb)
for frame in frames:
print(f"文件: {frame.filename}")
print(f" 行号: {frame.lineno}")
print(f" 函数: {frame.name}")
print(f" 代码: {frame.line}")
print(f" 位置: {frame}")
print()
demo_extract_tb()
def demo_format_list():
try:
int("abc")
except ValueError:
tb = sys.exc_info()[2]
frames = traceback.extract_tb(tb)
formatted = traceback.format_list(frames)
for line in formatted:
print(line, end="")
demo_format_list()
def custom_excepthook(exc_type, exc_value, exc_tb):
"""全局未捕获异常处理器 — 替代默认的 traceback 打印"""
error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
print(f"[自定义错误处理] {exc_type.__name__}: {exc_value}")
print(f"[详细信息] {error_msg}")
def demo_print_exception():
try:
raise ValueError("演示异常")
except ValueError:
exc_type, exc_value, exc_tb = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_tb)
demo_print_exception()
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class ExceptionUtils:
"""异常处理工具类"""
@staticmethod
def get_traceback_str(exc: Optional[BaseException] = None) -> str:
"""获取异常的完整 traceback 字符串"""
if exc is None:
return traceback.format_exc()
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
@staticmethod
def get_caller_info(depth: int = 1) -> dict:
"""获取调用者信息"""
frame = sys._getframe(depth + 1)
return {
"file": frame.f_code.co_filename,
"line": frame.f_lineno,
"function": frame.f_code.co_name,
}
@staticmethod
def log_exception(exc: BaseException, context: str = "", level: int = logging.ERROR):
"""记录异常到日志"""
tb_str = ExceptionUtils.get_traceback_str(exc)
caller = ExceptionUtils.get_caller_info()
logger.log(
level,
f"{context} — {type(exc).__name__}: {exc} "
f"at {caller['function']} ({caller['file']}:{caller['line']})\n{tb_str}"
)
try:
int("not_a_number")
except ValueError as e:
ExceptionUtils.log_exception(e, context="配置解析失败")
核心差异
| 维度 | Java | Python |
|---|
| 打印堆栈 | e.printStackTrace() | traceback.print_exc() |
| 堆栈转字符串 | StringWriter + PrintWriter | traceback.format_exc() |
| 获取堆栈帧 | e.getStackTrace() | traceback.extract_tb() |
| 全局异常处理 | Thread.setDefaultUncaughtExceptionHandler() | sys.excepthook |
| 堆栈过滤 | StackWalker (Java 9+) | 手动过滤 extract_tb() 结果 |
| 异常信息 | e.getMessage() | str(e) / e.args |
常见陷阱
try:
1 / 0
except ZeroDivisionError:
pass
error_str = traceback.format_exc()
try:
1 / 0
except ZeroDivisionError:
exc_info = sys.exc_info()
print(exc_info)
try:
1 / 0
except ZeroDivisionError:
exc_info = sys.exc_info()
if exc_info[0] is not None:
print(traceback.format_exception(*exc_info))
try:
int("abc")
except ValueError as e:
frames = traceback.extract_tb(e.__traceback__)
stack = traceback.extract_stack()
何时使用
traceback.print_exc(): 交互式调试、快速排查
traceback.format_exc(): 记录到日志、发送错误报告
traceback.extract_tb(): 需要程序化分析堆栈帧时
sys.excepthook: 全局异常拦截(如 GUI 应用、Web 框架)
logging.error(exc_info=True): 生产环境日志记录(推荐方式)
10.6 pdb/ipdb: Python 调试器
Java/Kotlin 对比
Python 实现
def calculate_average(numbers: list) -> float:
total = sum(numbers)
count = len(numbers)
return total / count
def debug_with_set_trace():
import pdb
x = 10
y = 20
result = x + y
return result
def pdb_commands_demo():
"""
在 pdb 交互模式中可用的命令:
导航:
n (next) — 执行当前行,不进入函数内部(Step Over)
s (step) — 执行当前行,进入函数内部(Step Into)
r (return) — 执行到当前函数返回
c (continue) — 继续执行到下一个断点
u (up) — 向上移动一层调用栈
d (down) — 向下移动一层调用栈
断点:
b (break) — 设置断点
b 42 — 在当前文件第 42 行设置断点
b function_name — 在函数入口设置断点
b file.py:42 — 在指定文件第 42 行设置断点
b — 列出所有断点
b 42, condition — 条件断点(condition 为布尔表达式)
cl (clear) — 清除断点
cl 1 — 清除 1 号断点
cl — 清除所有断点
查看变量:
p (print) — 打印表达式的值
p x — 打印变量 x
p x + y — 打印表达式
p type(x) — 打印类型
pp — 美化打印(pretty print)
a (args) — 打印当前函数的参数
w (where) — 打印调用栈(bt / backtrace 也可以)
执行代码:
!statement — 执行 Python 语句(修改变量、调用函数等)
!x = 100 — 修改变量值
!import os; os.getcwd()
其他:
l (list) — 查看当前代码上下文
ll (longlist) — 查看当前函数完整代码
h (help) — 帮助
q (quit) — 退出调试器
restart — 重新启动程序
"""
pass
def find_max_index(data: list) -> int:
"""找到列表中最大值的索引 — 有 bug"""
max_val = data[0]
max_idx = 0
for i in range(1, len(data)):
if data[i] > max_val:
max_val = data[i]
max_idx = i
return max_idx
def post_mortem_demo():
"""程序崩溃后,用 pdb 检查崩溃时的状态"""
import pdb
def buggy_function():
x = [1, 2, 3]
y = None
return x + y
try:
buggy_function()
except TypeError:
pdb.post_mortem()
def process_items(items: list):
"""处理大量数据时,只在特定条件下暂停"""
for i, item in enumerate(items):
if item is not None:
result = item * 2
else:
result = 0
return items
def custom_breakpoint():
"""替代默认的 pdb,添加额外功能"""
import pdb
import inspect
frame = inspect.currentframe()
caller_frame = frame.f_back
print(f"\n{'='*60}")
print(f"[断点] {caller_frame.f_code.co_name} "
f"({caller_frame.f_code.co_filename}:{caller_frame.f_lineno})")
print(f"{'='*60}")
print("局部变量:")
for name, value in caller_frame.f_locals.items():
print(f" {name} = {repr(value)}")
print(f"{'='*60}\n")
pdb.set_trace()
核心差异
| 维度 | Java (IDE) | Python (pdb) |
|---|
| 调试方式 | GUI(IDE 集成) | 命令行交互 |
| 设置断点 | 点击行号 | b 行号 / breakpoint() |
| 单步执行 | F7/F8 | s (step into) / n (step over) |
| 查看变量 | 鼠标悬停 | p 变量名 |
| 修改变量 | 右键 "Set Value" | !变量 = 新值 |
| 条件断点 | 右键断点 → Condition | b 行号, 条件 |
| 远程调试 | JDWP 协议 | pdb + rpdb (远程) |
| post-mortem | IDE 异常断点 | pdb.post_mortem() |
| 生产环境 | 远程调试 | breakpoint() + PYTHONBREAKPOINT=0 |
常见陷阱
def safe_breakpoint():
import os
if os.environ.get("DEBUG", "0") == "1":
breakpoint()
何时使用
breakpoint(): 开发环境快速调试,比 print 更高效
pdb.post_mortem(): 程序崩溃后分析现场(比看 traceback 更直观)
python -m pdb: 调试脚本或长时间运行的程序
PYTHONBREAKPOINT=0: 生产环境禁用断点
- IDE 调试器(VS Code / PyCharm): 复杂项目的首选,支持 GUI 断点、变量查看、多线程调试
- ipdb: 需要 Tab 补全和语法高亮时(
pip install ipdb)
10.7 3.11+ 改进的错误提示
Java/Kotlin 对比
List<String> list = new ArrayList<String>();
list.add(42);
String name = null;
System.out.println(name.length());
val name: String? = null
println(name!!.length())
Python 实现
def demo_multiline_error():
foods = [
{"name": "apple", "calories": 95},
{"name": "banana", "calories": 105},
{"name": "cherry", "calories": 50},
]
try:
result = (
foods[0]["calories"]
+ foods[1]["calories"]
+ foods[2]["missing"]
)
except KeyError as e:
print(f"KeyError: {e}")
demo_multiline_error()
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old"
def demo_name_error_suggestion():
"""模拟 3.12 的 NameError 建议"""
import math
try:
result = math.sqroot(16)
except AttributeError as e:
print(f"AttributeError: {e}")
demo_name_error_suggestion()
def demo_import_suggestions():
"""3.12 的 ImportError 改进"""
x = 42
pass
def version_comparison_demo():
"""展示不同版本的错误提示差异"""
config = {"db": {"host": "localhost"}}
try:
host = config["db"]["port"]["timeout"]
except KeyError as e:
print(f"\n[KeyError] {e}")
print("3.10: 只显示行号")
print("3.11: 显示 config[\"db\"][\"port\"][\"timeout\"] 中 \"port\" 处出错")
try:
"hello".uppper()
except AttributeError as e:
print(f"\n[AttributeError] {e}")
print("3.10: 'str' object has no attribute 'uppper'")
print("3.12: Did you mean: 'upper'?")
try:
"".join(sep="-", iterable=["a", "b", "c"])
except TypeError as e:
print(f"\n[TypeError] {e}")
print("3.10: join() got an unexpected keyword argument 'sep'")
print("3.12: Did you mean: 'sep' is not a valid argument. "
"str.join() takes exactly 1 argument (2 given)")
try:
result = math.pi
except NameError as e:
print(f"\n[NameError] {e}")
print("3.10: name 'math' is not defined")
print("3.12: Did you forget to import 'math'? "
"(or did you mean: '__import__'?)")
version_comparison_demo()
核心差异
| 维度 | Java | Kotlin | Python 3.10 | Python 3.11+ | Python 3.12+ |
|---|
| 错误位置 | 精确到列 | 精确到列 | 精确到行 | 精确到子表达式 | 精确到子表达式 |
| 拼写建议 | 无 | 编译期有 | 无 | 部分 | 大幅改进 |
| 导入建议 | IDE 提供 | IDE 提供 | 无 | 无 | 有 |
| 彩色输出 | IDE 提供 | IDE 提供 | 无 | 无 | 默认彩色 |
| Helpful NPE | 有 (14+) | 有 | 无 | 无 | 无 |
| 参数名建议 | 有 | 有 | 无 | 无 | 有 |
常见陷阱
何时使用
- 3.11+: 升级到 3.11 的最大理由之一就是错误提示改进,开发效率显著提升
- 3.12+: 如果你的项目可以升级,3.12 的拼写建议对新手特别友好
- 3.13+: 彩色 traceback 是锦上添花,不影响功能
- 生产环境: 错误提示改进只影响开发体验,不影响运行时行为
- 最低版本: 如果必须支持 3.10,用
sys.version_info 检查版本
import sys
if sys.version_info >= (3, 12):
print("享受改进的错误提示和拼写建议")
elif sys.version_info >= (3, 11):
print("享受精确的错误位置定位")
else:
print("考虑升级到 3.11+ 以获得更好的错误提示")