蚊章汇 - Liran Haimovitch
type: channel
link: https://linuxcn.bwsl.wang/author/1374/
description: Liran Haimovitch
items: 在 Python 调试过程中设置不中断的断点

在 Python 调试过程中设置不中断的断点
type: item
date: 2020-03-12T18:45:00+08:00
description: 你对如何让调试器变得更快产生过兴趣吗？本文将分享我们在为 Python 构建调试器时得到的一些经验。整段故事讲的是我们在 Rookout 公司的团队为 Python 调试器开发不中断断点的经历，以及开发过程中得到的经验。我将在本月于旧金山举办的 PyBay 2019 上介绍有关 Python 调试过程的更多细节，但现在就让我们立刻开始这段故事。Python 调试器的心脏：sys.set\_trace在诸多可选的 Python 调试器中，使用最广泛的三个是：pdb，它是 Python 标准库的一部分PyDev，它是内嵌在 Eclipse 和 Pycharm 等 IDE 中的调试器ipdb，它是 IPython 的调试器Python 调试器的选择虽多，但它们几乎都基于同一个函数：sys.settrace。 值得一提的是， sys.settrace 可能也是 Python 标准库中最复杂的函数。简单来讲，settrace 的作用是为解释器注册一个跟踪函数，它在下列四种情形发生时被调用：函数调用语句执行函数返回异常抛出一个简单的跟踪函数看上去大概是这样：def simple_tracer(frame, event, arg): co = frame.f_code func_name = co.co_name line_no = frame.f_lineno print("{e} {f} {l}".format( e=event, f=func_name, l=line_no)) return simple_tracer在分析函数时我们首先关注的是参数和返回值，该跟踪函数的参数分别是：frame，当前堆栈帧，它是包含当前函数执行时解释器里完整状态的对象event，事件，它是一个值可能为 call、line、return 或 exception 的字符串arg，参数，它的取值基于 event 的类型，是一个可选项该跟踪函数的返回值是它自身，这是由于解释器需要持续跟踪两类跟踪函数：全局跟踪函数（每线程）：该跟踪函数由当前线程调用 sys.settrace 来设置，并在解释器创建一个新的堆栈帧时被调用（即代码中发生函数调用时）。虽然没有现成的方式来为不同的线程设置跟踪函数，但你可以调用 threading.settrace 来为所有新创建的 threading 模块线程设置跟踪函数。局部跟踪函数（每一帧）：解释器将该跟踪函数的值设置为全局跟踪函数创建帧时的返回值。同样也没有现成的方法能够在帧被创建时自动设置局部跟踪函数。该机制的目的是让调试器对被跟踪的帧有更精确的把握，以减少对性能的影响。简单三步构建调试器 （我们最初的设想）仅仅依靠上文提到的内容，用自制的跟踪函数来构建一个真正的调试器似乎有些不切实际。幸运的是，Python 的标准调试器 pdb 是基于 Bdb 构建的，后者是 Python 标准库中专门用于构建调试器的基类。基于 Bdb 的简易断点调试器看上去是这样的：import bdb import inspect class Debugger(bdb.Bdb): def __init__(self): Bdb.__init__(self) self.breakpoints = dict() self.set_trace() def set_breakpoint(self, filename, lineno, method): self.set_break(filename, lineno) try : self.breakpoints[(filename, lineno)].add(method) except KeyError: self.breakpoints[(filename, lineno)] = [method] def user_line(self, frame): if not self.break_here(frame): return # Get filename and lineno from frame (filename, lineno, _, _, _) = inspect.getframeinfo(frame) methods = self.breakpoints[(filename, lineno)] for method in methods: method(frame)这个调试器类的全部构成是：继承 Bdb，定义一个简单的构造函数来初始化基类，并开始跟踪。添加 set_breakpoint 方法，它使用 Bdb 来设置断点，并跟踪这些断点。重载 Bdb 在当前用户行调用的 user_line 方法，该方法一定被一个断点调用，之后获取该断点的源位置，并调用已注册的断点。这个简易的 Bdb 调试器效率…