Python 调试代码有多种方法,其中一种方法是在代码中您希望调用 Python 调试器的位置设置断点。用于在不同调用点进入调试会话的语句取决于您使用的 Python 解释器版本,在本教程中我们将看到这一点。
在本教程中,您将发现设置不同 Python 版本断点的各种方法。
完成本教程后,您将了解:
- 如何在早期 Python 版本中调用
pdb
调试器 - 如何在 Python 3.7 中使用新的内置 breakpoint() 函数
- 如何编写自己的 breakpoint() 函数以简化早期 Python 版本的调试过程
- 如何使用事后调试器
使用我新书《Python for Machine Learning》快速启动您的项目,书中包含分步教程和所有示例的Python源代码文件。
让我们开始吧。教程概述
本教程分为三个部分;它们是:
- 在 Python 代码中设置断点
- 在早期 Python 版本中调用 pdb 调试器
- 在 Python 3.7 中使用 breakpoint() 函数
- 为早期 Python 版本编写自己的 breakpoint() 函数
- breakpoint() 函数的限制
在 Python 代码中设置断点
我们之前已经看到,调试 Python 脚本的一种方法是在命令行中使用 Python 调试器运行它。
为此,我们需要使用 -m pdb 命令,该命令在执行 Python 脚本之前加载 pdb 模块。在同一个命令行界面中,然后我们会跟上一个您选择的特定调试器命令,例如 n 前进到下一行,或者如果您打算进入一个函数,则使用 s。
随着代码长度的增加,此方法很快就会变得麻烦。解决此问题并更好地控制中断代码位置的一种方法是直接在代码中插入断点。
在早期 Python 版本中调用 pdb 调试器
在 Python 3.7 之前的版本中调用 pdb 调试器,您需要 import pdb 并调用 pdb.set_trace() 到您希望进入交互式调试会话的代码点。
如果我们以 实现通用注意力机制 的代码为例,我们可以如下中断代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
from numpy import array from numpy import random from numpy import dot from scipy.special import softmax # 导入 Python 调试模块 import pdb # 四个不同单词的编码表示 word_1 = array([1, 0, 0]) word_2 = array([0, 1, 0]) word_3 = array([1, 1, 0]) word_4 = array([0, 0, 1]) # 将词嵌入堆叠到单个数组中 words = array([word_1, word_2, word_3, word_4]) # 生成权重矩阵 random.seed(42) W_Q = random.randint(3, size=(3, 3)) W_K = random.randint(3, size=(3, 3)) W_V = random.randint(3, size=(3, 3)) # 生成查询、键和值 Q = dot(words, W_Q) K = dot(words, W_K) V = dot(words, W_V) # 插入断点 pdb.set_trace() # 计算查询向量与所有键向量的分数 scores = dot(Q, K.transpose()) # 通过 softmax 操作计算权重 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) # 通过值向量的加权和计算注意力 attention = dot(weights, V) print(attention) |
执行脚本后,pdb
调试器会在我们计算 scores
变量之前启动,我们可以继续发出任何您选择的调试器命令,例如 n 前进到下一行,或 c 继续执行
1 2 3 4 5 6 7 8 9 10 |
/Users/mlm/main.py(33)<module>() -> scores = dot(Q, K.transpose()) (Pdb) n > /Users/mlm/main.py(36)<module>() -> weights = softmax(scores / K.shape[1] ** 0.5, axis=1) (Pdb) c [[0.98522025 1.74174051 0.75652026] [0.90965265 1.40965265 0.5 ] [0.99851226 1.75849334 0.75998108] [0.99560386 1.90407309 0.90846923]] |
虽然有效,但这并不是在代码中插入断点的最优雅和最直观的方法。Python 3.7 实现了一种更直接的方法,我们将在接下来看到。
在 Python 3.7 中使用 breakpoint() 函数
Python 3.7 包含一个内置的 breakpoint() 函数,该函数在调用点(或放置 breakpoint() 语句的代码位置)进入 Python 调试器。
调用时,breakpoint() 函数的默认实现将调用 sys.breakpointhook(),后者又会调用 pdb.set_trace() 函数。这很方便,因为我们无需 import pdb 并显式调用 pdb.set_trace() 。
让我们重新审视实现通用注意力机制的代码,并用 breakpoint() 语句引入断点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
from numpy import array from numpy import random from scipy.special import softmax # 四个不同单词的编码表示 word_1 = array([1, 0, 0]) word_2 = array([0, 1, 0]) word_3 = array([1, 1, 0]) word_4 = array([0, 0, 1]) # 将词嵌入堆叠到单个数组中 words = array([word_1, word_2, word_3, word_4]) # 生成权重矩阵 random.seed(42) W_Q = random.randint(3, size=(3, 3)) W_K = random.randint(3, size=(3, 3)) W_V = random.randint(3, size=(3, 3)) # 生成查询、键和值 Q = words @ W_Q K = words @ W_K V = words @ W_V # 插入断点 breakpoint() # 计算查询向量与所有键向量的分数 scores = Q @ K.transpose() # 通过 softmax 操作计算权重 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) # 通过值向量的加权和计算注意力 attention = weights @ V print(attention) |
使用 breakpoint() 函数的一个优点是,通过调用 sys.breakpointhook() 的默认实现,会查询一个新的环境变量 PYTHONBREAKPOINT。这个环境变量可以取不同的值,基于这些值可以执行不同的操作。
例如,将 PYTHONBREAKPOINT 的值设置为 0 会禁用所有断点。因此,您的代码可以包含任意数量的断点,但这些断点可以轻松地停止执行代码,而无需物理移除它们。例如,如果包含该代码的脚本名称是main.py,我们可以通过在命令行界面中这样调用它来禁用所有断点
1 |
PYTHONBREAKPOINT=0 python main.py |
否则,我们可以通过在代码本身中设置环境变量来实现相同的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
from numpy import array from numpy import random from scipy.special import softmax # 设置 PYTHONBREAKPOINT 环境变量的值 import os os.environ['PYTHONBREAKPOINT'] = '0' # 四个不同单词的编码表示 word_1 = array([1, 0, 0]) word_2 = array([0, 1, 0]) word_3 = array([1, 1, 0]) word_4 = array([0, 0, 1]) # 将词嵌入堆叠到单个数组中 words = array([word_1, word_2, word_3, word_4]) # 生成权重矩阵 random.seed(42) W_Q = random.randint(3, size=(3, 3)) W_K = random.randint(3, size=(3, 3)) W_V = random.randint(3, size=(3, 3)) # 生成查询、键和值 Q = words @ W_Q K = words @ W_K V = words @ W_V # 插入断点 breakpoint() # 计算查询向量与所有键向量的分数 scores = Q @ K.transpose() # 通过 softmax 操作计算权重 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) # 通过值向量的加权和计算注意力 attention = weights @ V print(attention) |
PYTHONBREAKPOINT 的值会在每次调用 sys.breakpointhook() 时被查询。这意味着这个环境变量的值可以在代码执行期间更改,并且 breakpoint() 函数也会相应地做出响应。
PYTHONBREAKPOINT 环境变量也可以设置为其他值,例如可调用对象的名称。假设,例如,我们想使用 pdb 以外的其他 Python 调试器,例如 ipdb(如果调试器尚未安装,请先运行 pip install ipdb)。在这种情况下,我们可以在命令行界面中调用 main.py 脚本,并在不更改代码本身的情况下挂载调试器
1 |
PYTHONBREAKPOINT=ipdb.set_trace python main.py |
这样做,breakpoint() 函数将在下一个调用点进入 ipdb 调试器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(33)<module>() 32 # 计算查询向量与所有键向量的分数 ---> 33 scores = Q @ K.transpose() 34 ipdb> n > /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(36)<module>() 35 # 通过 softmax 操作计算权重 ---> 36 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) 37 ipdb> c [[0.98522025 1.74174051 0.75652026] [0.90965265 1.40965265 0.5 ] [0.99851226 1.75849334 0.75998108] [0.99560386 1.90407309 0.90846923]] |
该函数还可以接受 breakpoint(*args, **kwargs) 作为输入参数,然后将这些参数传递给 sys.breakpointhook()。这是因为任何可调用对象(例如第三方调试器模块)都可能接受可选参数,这些参数可以通过 breakpoint() 函数传递。
想开始学习机器学习 Python 吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
编写您自己的 Python 早期版本中的 breakpoint() 函数
让我们回到 Python 3.7 及更早版本没有现成的 breakpoint() 函数的事实。我们可以自己编写。
与 Python 3.7 及更高版本中实现 breakpoint() 函数的方式类似,我们可以实现一个检查环境变量值并在
- 如果环境变量的值设置为 0,则跳过代码中的所有断点。
- 如果环境变量是空字符串,则进入默认的 Python pdb 调试器。
- 根据环境变量的值进入另一个调试器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
... # 定义我们的 breakpoint() 函数 def breakpoint(*args, **kwargs): import importlib # 读取环境变量的值 val = os.environ.get('PYTHONBREAKPOINT') # 如果值已设置为 0,则跳过所有断点 if val == '0': return None # 否则,如果值为一个空字符串,则调用默认的 pdb 调试器 elif len(val) == 0: hook_name = 'pdb.set_trace' # else, assign the value of the environment variable else: hook_name = val # split the string into the module name and the function name mod, dot, func = hook_name.rpartition('.') # get the function from the module module = importlib.import_module(mod) hook = getattr(module, func) return hook(*args, **kwargs) ... |
我们可以将此函数包含在代码中并运行它(在这种情况下,使用 Python 2.7 解释器)。如果我们将来设置环境变量的值为空字符串,我们会发现 pdb 调试器会停在代码中我们放置 `breakpoint()` 函数的那一点。然后我们可以从那里开始在命令行中输入调试器命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
from numpy import array from numpy import random from numpy import dot from scipy.special import softmax # setting the value of the environment variable import os os.environ['PYTHONBREAKPOINT'] = '' # 定义我们的 breakpoint() 函数 def breakpoint(*args, **kwargs): import importlib # 读取环境变量的值 val = os.environ.get('PYTHONBREAKPOINT') # 如果值已设置为 0,则跳过所有断点 if val == '0': return None # 否则,如果值为一个空字符串,则调用默认的 pdb 调试器 elif len(val) == 0: hook_name = 'pdb.set_trace' # else, assign the value of the environment variable else: hook_name = val # split the string into the module name and the function name mod, dot, func = hook_name.rpartition('.') # get the function from the module module = importlib.import_module(mod) hook = getattr(module, func) return hook(*args, **kwargs) # 四个不同单词的编码表示 word_1 = array([1, 0, 0]) word_2 = array([0, 1, 0]) word_3 = array([1, 1, 0]) word_4 = array([0, 0, 1]) # 将词嵌入堆叠到单个数组中 words = array([word_1, word_2, word_3, word_4]) # 生成权重矩阵 random.seed(42) W_Q = random.randint(3, size=(3, 3)) W_K = random.randint(3, size=(3, 3)) W_V = random.randint(3, size=(3, 3)) # 生成查询、键和值 Q = dot(words, W_Q) K = dot(words, W_K) V = dot(words, W_V) # 插入断点 breakpoint() # 计算查询向量与所有键向量的分数 scores = dot(Q, K.transpose()) # 通过 softmax 操作计算权重 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) # 通过值向量的加权和计算注意力 attention = dot(weights, V) print(attention) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(32)breakpoint()->None -> return hook(*args, **kwargs) (Pdb) n > /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(59)<module>() -> scores = dot(Q, K.transpose()) (Pdb) n > /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(62)<module>() -> weights = softmax(scores / K.shape[1] ** 0.5, axis=1) (Pdb) c [[0.98522025 1.74174051 0.75652026] [0.90965265 1.40965265 0.5 ] [0.99851226 1.75849334 0.75998108] [0.99560386 1.90407309 0.90846923]] |
Similarly, if we set the environment variable to
1 |
os.environ['PYTHONBREAKPOINT'] = 'ipdb.set_trace' |
The `breakpoint()` function that we have implemented now enters the ipdb debugger and stops at the call site
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(31)breakpoint() 30 ---> 31 return hook(*args, **kwargs) 32 ipdb> n > /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(58)<module>() 57 # scoring the query vectors against all key vectors ---> 58 scores = dot(Q, K.transpose()) 59 ipdb> n > /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(61)<module>() 60 # computing the weights by a softmax operation ---> 61 weights = softmax(scores / K.shape[1] ** 0.5, axis=1) 62 ipdb> c [[0.98522025 1.74174051 0.75652026] [0.90965265 1.40965265 0.5 ] [0.99851226 1.75849334 0.75998108] [0.99560386 1.90407309 0.90846923]] |
Setting the environment variable to 0 simply skips all breakpoints, and the computed attention output is returned in the command line, as expected
1 |
os.environ['PYTHONBREAKPOINT'] = '0' |
1 2 3 4 |
[[0.98522025 1.74174051 0.75652026] [0.90965265 1.40965265 0.5 ] [0.99851226 1.75849334 0.75998108] [0.99560386 1.90407309 0.90846923]] |
This facilitates the process of breaking into the code for Python versions earlier than v3.7 because it now becomes a matter of setting the value of an environment variable rather than having to manually introduce (or remove) the `import pdb; pdb.set_trace()` statement at different call sites in the code.
breakpoint() 函数的限制
The `breakpoint()` function allows you to bring in the debugger at some point in the program. You need to find the exact position that you need the debugger to put the breakpoint into it. If you consider the following code
1 2 3 4 5 |
try: func() except: breakpoint() print("exception!") |
This will bring you the debugger when the function `func()` raised exceptions. It can be triggered by the function itself or deep inside some other functions that it calls. But the debugger will start at the line `print("exception!")` above, which may not be very useful.
The way that we can bring up the debugger at the point of exception is called the post-mortem debugger. It works by asking Python to register the debugger `pdb.pm()` as the exception handler when an uncaught exception is raised. When it is called, it will look for the last exception raised and start the debugger at that point. To use the post-mortem debugger, we just need to add the following code before the program is run
1 2 3 4 5 6 |
import sys import pdb def debughook(etype, value, tb): pdb.pm() # post-mortem debugger sys.excepthook = debughook |
This is handy because nothing else needs to be changed in the program. For example, assume we want to evaluate the average of $1/x$ using the following program. It is quite easy to overlook some corner cases, but we can catch the issue when an exception is raised
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import sys import pdb import random def debughook(etype, value, tb): pdb.pm() # post-mortem debugger sys.excepthook = debughook # Experimentally find the average of 1/x where x is a random integer in 0 to 9999 N = 1000 randomsum = 0 for i in range(N): x = random.randint(0,10000) randomsum += 1/x print("Average is", randomsum/N) |
When we run the above program, the program may terminate, or it may raise a division by zero exception, depending on whether the random number generator ever produces zero in the loop. In that case, we may see the following
1 2 3 4 5 6 |
> /Users/mlm/py_pmhook.py(17)<module>() -> randomsum += 1/x (Pdb) p i 16 (Pdb) p x 0 |
where we found the exception is raised at which line, and we can check the value of the variables as we can usually do in `pdb`.
In fact, it is more convenient to print the traceback and the exception when the post-mortem debugger is launched
1 2 3 4 5 6 7 8 9 |
import sys import pdb import traceback def debughook(etype, value, tb): traceback.print_exception(etype, value, tb) print() # make a new line before launching post-mortem pdb.pm() # post-mortem debugger sys.excepthook = debughook |
And the debugger session will be started as follows
1 2 3 4 5 6 7 8 |
Traceback (most recent call last): File "/Users/mlm/py_pmhook.py", line 17, in <module> randomsum += 1/x ZeroDivisionError: division by zero > /Users/mlm/py_pmhook.py(17)<module>() -> randomsum += 1/x (Pdb) |
进一步阅读
如果您想深入了解,本节提供了更多关于该主题的资源。
网站
- Python pdb 模块,https://docs.pythonlang.cn/3/library/pdb.html
- Python 内置的 breakpoint(),https://pythonlang.cn/dev/peps/pep-0553/
总结
In this tutorial, you discovered various ways of setting breakpoints in different versions of Python.
具体来说,你学到了:
- 如何调用早期 Python 版本的 pdb 调试器
- 如何使用 Python 3.7 中引入的新内置 `breakpoint()` 函数
- 如何编写自己的 `breakpoint()` 函数以简化早期 Python 版本的调试过程
你有什么问题吗?
在下面的评论中提出您的问题,我将尽力回答。
暂无评论。