如何在Python中设置断点和异常钩子

695 阅读11分钟

在Python中调试代码有不同的方法,其中之一是在希望调用Python调试器的地方向代码引入断点。在不同的调用点进入调试会话的语句取决于所使用的Python解释器的版本,我们将在本教程中看到这一点。

在本教程中,你将发现在不同版本的 Python 中设置断点的各种方法。

完成本教程后,你将知道。

  • 如何在早期版本的 Python 中调用pdb 调试器。
  • 如何利用 Python 3.7 中引入的新的、内置的breakpoint()函数。
  • 如何编写你自己的breakpoint()函数来简化早期版本的 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,并在你的代码中想进入交互式调试会话的地方调用pdb.set_trace()。

如果我们重新考虑一下,我们可以按如下方式闯入代码。

from numpy import array
from numpy import random
from numpy import dot
from scipy.special import softmax

# importing the Python debugger module
import pdb

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
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))

# generating the queries, keys and values
Q = dot(words, W_Q)
K = dot(words, W_K)
V = dot(words, W_V)

# inserting a breakpoint
pdb.set_trace()

# scoring the query vectors against all key vectors
scores = dot(Q, K.transpose())

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = dot(weights, V)

print(attention)

现在,在我们计算变量scores 之前,执行脚本会打开pdb 调试器,我们可以继续发布任何选择的调试器命令,比如n移到下一行,或者c继续执行。

/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 调试器。

当调用时,断点()函数的默认实现将调用sys.breakpointhook(),而后者又调用pdb.set_trace()函数。这很方便,因为我们将不需要导入 pdb,也不需要自己明确地调用pdb.set_trace()。

让我们重新考虑一下实现一般注意力机制的代码,现在通过breakpoint()语句引入一个断点。

from numpy import array
from numpy import random
from scipy.special import softmax

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
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))

# generating the queries, keys and values
Q = words @ W_Q
K = words @ W_K
V = words @ W_V

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = Q @ K.transpose()

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = weights @ V

print(attention)

使用breakpoint()函数的一个好处是,在调用sys.breakpointhook()的默认实现时,会查询一个新的环境变量PYTHONBREAKPOINT的值。这个环境变量可以取不同的值,在此基础上可以执行不同的操作。

例如,将PYTHONBREAKPOINT的值设置为0,就可以禁用所有断点。因此,你的代码可以包含尽可能多的断点,但这些断点可以很容易地被阻止, ,停止代码的执行,而不需要从物理上删除它们。如果(例如)包含代码的脚本名称是main.py,我们将通过在命令行界面中调用它来禁用所有断点,如下所示。

PYTHONBREAKPOINT=0 python main.py

否则,我们可以通过在代码本身设置环境变量来达到同样的结果。

from numpy import array
from numpy import random
from scipy.special import softmax

# setting the value of the PYTHONBREAKPOINT environment variable
import os
os.environ['PYTHONBREAKPOINT'] = '0'

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
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))

# generating the queries, keys and values
Q = words @ W_Q
K = words @ W_K
V = words @ W_V

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = Q @ K.transpose()

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = weights @ V

print(attention)

每次调用sys.breakpointhook()时都会查询PYTHONBREAKPOINT的值。这意味着这个环境变量的值可以在代码执行过程中被改变,断点()函数会做出相应的反应。

PYTHONBREAKPOINT环境变量也可以被设置为其他值,比如设置为一个可调用的名称。比如说,我们想使用 pdb 以外的另一个 Python 调试器,比如 ipdb (如果调试器还没有安装,先运行pip install ipdb)。在这种情况下,我们将在命令行界面中调用main.py脚本,并钩住调试器,而不对代码本身做任何修改。

PYTHONBREAKPOINT=ipdb.set_trace python main.py

这样做,breakpoint()函数在下一个调用点进入ipdb调试器。

> /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(33)<module>()
     32 # scoring the query vectors against all key vectors
---> 33 scores = Q @ K.transpose()
     34 

ipdb> n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(36)<module>()
     35 # computing the weights by a softmax operation
---> 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, **kws),然后将其传递给sys.breakpointhook()。这是因为任何可调用程序(如第三方调试器模块)都可能接受可选的参数,这些参数可以通过breakpoint()函数传递。

在早期版本的 Python 中编写你自己的 breakpoint() 函数

让我们回到这样一个事实:早于 v3.7 的 Python 版本并没有轻易地内置断点()函数。我们可以编写自己的。

类似于从Python 3.7开始的breakpoint()函数的实现方式,我们可以实现一个检查环境变量值的函数,并且。

  • 如果环境变量的值被设置为0,则跳过代码中的所有断点。
  • 如果环境变量为空字符串,则进入默认的 Python pdb 调试器。
  • 进入另一个由环境变量值指定的调试器。
...

# defining our breakpoint() function
def breakpoint(*args, **kwargs):
    import importlib
    # reading the value of the environment variable
    val = os.environ.get('PYTHONBREAKPOINT')
    # if the value has been set to 0, skip all breakpoints
    if val == '0':
        return None
    # else if the value is an empty string, invoke the default pdb debugger
    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调试器会在我们放置断点()函数的代码中的那一点停止。然后我们就可以从那里开始在命令行中发出调试器命令。

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'] = ''


# defining our breakpoint() function
def breakpoint(*args, **kwargs):
    import importlib
    # reading the value of the environment variable
    val = os.environ.get('PYTHONBREAKPOINT')
    # if the value has been set to 0, skip all breakpoints
    if val == '0':
        return None
    # else if the value is an empty string, invoke the default pdb debugger
    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)


# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
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))

# generating the queries, keys and values
Q = dot(words, W_Q)
K = dot(words, W_K)
V = dot(words, W_V)

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = dot(Q, K.transpose())

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = dot(weights, V)

print(attention)
> /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]]

同样地,如果我们把环境变量设置为。

os.environ['PYTHONBREAKPOINT'] = 'ipdb.set_trace'

我们实现的断点()函数现在进入了ipdb调试器,并停在了调用位置。

> /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]]

将环境变量设置为0,就会简单地跳过所有的断点,计算出的注意输出就会在命令行中返回,正如预期的那样。

os.environ['PYTHONBREAKPOINT'] = '0'
[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

这促进了对早于v3.7版本的Python代码的破解过程,因为现在变成了一个设置环境变量值的问题,而不是在代码的不同调用位置手动引入(或删除)import pdb; pdb.set_trace()语句。

breakpoint() 函数的局限性

breakpoint() 函数允许你在程序的某个位置引入调试器。你需要找到你需要调试器的确切位置,将断点放入其中。如果你考虑下面的代码。

try:
    func()
except:
    breakpoint()
    print("exception!")

这将在函数func() 引起异常时带给你调试器。它可以由函数本身触发,也可以深入到它所调用的一些其他函数中。但调试器将从上面的行print("exception!") 开始。这可能不是很有用。

我们可以在异常点唤起调试器的方法叫做死后调试器。它的工作原理是要求Python将调试器pdb.pm() 作为异常处理程序注册,当未捕获的异常被引发时。当它被调用时,它将寻找最后一个异常的发生点,并在该点上启动调试器。要使用死后调试器,我们只需要在程序运行前添加以下代码。

import sys
import pdb

def debughook(etype, value, tb):
    pdb.pm() # post-mortem debugger
sys.excepthook = debughook

这很方便,因为不需要改变程序中的其他内容。举个例子,假设我们想用下面的程序来评估1/x$的平均值。这很容易忽略一些角落的情况,但我们可以在出现异常时抓住这个问题。

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)

当我们运行上述程序时,程序可能终止,也可能引发除以零的异常,这取决于随机数发生器在循环中是否产生过零。在这种情况下,我们可能会看到下面的情况。

> /Users/mlm/py_pmhook.py(17)<module>()
-> randomsum += 1/x
(Pdb) p i
16
(Pdb) p x
0

其中我们发现异常是在哪一行引发的,我们可以像通常在pdb ,检查变量的值。

事实上,在启动死后调试器时,打印回溯和异常更为方便。

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

并且调试器会话将按如下方式启动。

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 中设置断点的各种方法。

具体来说,你学到了

  • 如何在早期版本的 Python 中调用 pdb 调试器。
  • 如何利用 Python 3.7 中引入的新的、内置的breakpoint()函数。
  • 如何编写你自己的breakpoint()函数来简化早期版本的 Python 的调试过程。