上一章向您展示了如何在代码中添加日志记录和测试,但是无论您有多少测试,您总是会有 bug。最大的问题总是用户输入,因为根本不可能测试所有可能的输入,这意味着在某一点上,我们需要调试代码。
有许多调试技术,最肯定的是,您已经使用了其中的一些。在本章中,我们将重点介绍打印/跟踪调试和交互式调试。
使用打印语句、堆栈跟踪和日志进行调试是最通用的方法之一,而且很可能是您使用过的第一种调试类型。甚至一个print 'Hello world'也可以被认为是这种类型,因为输出将显示您的代码正在正确执行。显然,解释如何以及在何处放置 print 语句来调试代码是没有意义的,但是使用 decorator 和其他 Python 模块有很多不错的技巧,使这种类型的调试更加有用,例如faulthandler。
交互式调试是一种更复杂的调试方法。它允许您在程序仍在运行时调试程序。使用此方法,甚至可以在应用运行时更改变量,并在所需的任意点暂停应用。缺点是它需要一些关于调试器命令的知识才能真正有用。
总而言之,我们将涵盖以下主题:
- 使用
print、trace、logging、faulthandler进行调试 - 使用
pdb进行交互式调试
最基本的调试形式是在代码中添加一个简单的 print 语句,以查看哪些仍在工作,哪些不工作。这在各种情况下都很有用,可能有助于解决大多数问题。在本章后面,我们将展示一些交互式调试方法,但这些方法并不总是适用的。在多线程环境中,交互式调试往往变得困难甚至不可能,而在关闭的远程服务器上,您可能还需要不同的解决方案。这两种方法都有各自的优点,但我个人 90%的时间选择非交互式调试,因为一个简单的打印/日志语句通常足以分析问题的原因。
生成器的一个基本示例(我也做过类似的工作)如下所示:
>>> def spam_generator():
... print('a')
... yield 'spam'
... print('b')
... yield 'spam!'
... print('c')
... yield 'SPAM!'
... print('d')
>>> generator = spam_generator()
>>> next(generator)
a
'spam'
>>> next(generator)
b
'spam!'这准确地显示了代码所处的位置,并且因此无法到达。如果没有这个示例,您可能会期望第一次打印在spam_generator()调用之后立即出现,因为它是一个生成器。然而,在我们yield得到一个项目之前,执行完全停滞。假设您在第一个yield之前有一些设置代码,它将在实际调用next之前不会运行。
虽然这是使用 print 语句调试函数的最简单方法之一,但绝对不是最好的方法。我们可以先制作一个自动打印功能,自动增加字母:
>>> import string
>>> def print_character():
... i = 0
... while True:
... print('Letter: %r' % string.ascii_letters[i])
... i = (i + 1) % len(string.ascii_letters)
... yield
>>> # Always initialize
>>> print_character = print_character()
>>> next(print_character)
Letter: 'a'
>>> next(print_character)
Letter: 'b'
>>> next(print_character)
Letter: 'c'尽管打印语句生成器略优于裸打印语句,但它的帮助还不大。在运行代码时,查看实际执行了哪些行会更有用。我们可以使用inspect.currentframe手动完成,但不需要黑客。Python 为您介绍了一些专用工具。
简单的打印语句在很多情况下都很有用,因为您几乎可以在每个应用中轻松地合并打印语句。无论它是远程的还是本地的、线程化的还是使用多进程的,这都无关紧要。它几乎在任何地方都能工作,使它成为除日志记录之外最通用的解决方案。然而,一般解决方案往往不是最佳解决方案。对于最常见的场景,有更好的解决方案。其中之一是trace模块。它为您提供了一种跟踪每次执行、函数之间的关系以及其他一些情况的方法。
为了演示,我们将使用前面的代码,但不使用打印语句:
def eggs_generator():
yield 'eggs'
yield 'EGGS!'
def spam_generator():
yield 'spam'
yield 'spam!'
yield 'SPAM!'
generator = spam_generator()
print(next(generator))
print(next(generator))
generator = eggs_generator()
print(next(generator))我们将通过跟踪模块执行:
# python3 -m trace --trace --timing tracing.py
--- modulename: tracing, funcname: <module>
0.00 tracing.py(1): def eggs_generator():
0.00 tracing.py(6): def spam_generator():
0.00 tracing.py(11): generator = spam_generator()
0.00 tracing.py(12): print(next(generator))
--- modulename: tracing, funcname: spam_generator
0.00 tracing.py(7): yield 'spam'
spam
0.00 tracing.py(13): print(next(generator))
--- modulename: tracing, funcname: spam_generator
0.00 tracing.py(8): yield 'spam!'
spam!
0.00 tracing.py(15): generator = eggs_generator()
--- modulename: tracing, funcname: spam_generator
0.00 tracing.py(16): print(next(generator))
--- modulename: tracing, funcname: eggs_generator
0.00 tracing.py(2): yield 'eggs'
eggs
--- modulename: trace, funcname: _unsettrace
0.00 trace.py(77): sys.settrace(None)很好,不是吗?它准确地显示了使用函数名执行的行,更重要的是,哪一行是由哪一条语句(或多条语句)引起的。此外,它还显示了相对于程序的开始时间执行它的时间。这是由于--timing标志。
正如您可能期望的那样,这个输出有点过于冗长,无法普遍使用。尽管您可以通过使用命令行参数选择忽略特定模块和目录,但在许多情况下仍然过于冗长。那么,让我们来看下一个解决方案——上下文管理器。前面的输出已经揭示了一些trace内部结构。最后一行显示了一个sys.settrace调用,这正是我们手动跟踪所需要的:
import- sys
import trace as trace_module
import contextlib
@contextlib.contextmanager
def trace(count=False, trace=True, timing=True):
tracer = trace_module.Trace(
count=count, trace=trace, timing=timing)
sys.settrace(tracer.globaltrace)
yield tracer
sys.settrace(None)
result = tracer.results()
result.write_results(show_missing=False, summary=True)
def eggs_generator():
yield 'eggs'
yield 'EGGS!'
def spam_generator():
yield 'spam'
yield 'spam!'
yield 'SPAM!'
with trace():
generator = spam_generator()
print(next(generator))
print(next(generator))
generator = eggs_generator()
print(next(generator))当作为常规 Python 文件执行时,返回:
# python3 tracing.py
--- modulename: tracing, funcname: spam_generator
0.00 tracing.py(24): yield 'spam'
spam
--- modulename: tracing, funcname: spam_generator
0.00 tracing.py(25): yield 'spam!'
spam!
--- modulename: contextlib, funcname: __exit__
0.00 contextlib.py(64): if type is None:
0.00 contextlib.py(65): try:
0.00 contextlib.py(66): next(self.gen)
--- modulename: tracing, funcname: trace
0.00 tracing.py(12): sys.settrace(None)这个代码立即揭示了跟踪代码在内部的作用:它使用sys.settrace告诉 Python 解释器将要执行的每个语句发送到哪里。考虑到这一点,作为装饰器编写函数显然是微不足道的,但如果您需要,我将把它作为练习留给您。
另一个好处是,通过包装tracer.globaltrace,您可以轻松地向跟踪函数添加额外的过滤器。该函数采用以下参数(来自标准 Python 文档):
参数
|
描述
|
| --- | --- |
| Call | 调用一个函数(或输入一些其他代码块)。调用全局跟踪函数;arg为None。返回值指定本地跟踪函数。 |
| Line | 解释器即将执行一行新代码或重新执行循环的条件。调用本地跟踪函数;arg为None。返回值指定新的本地跟踪函数。有关如何工作的详细说明,请参见Objects/lnotab_notes.txt 。 |
| return | 一个函数(或另一个代码块)即将返回。调用本地跟踪函数;arg是将返回的值,如果事件是由引发的异常引起的,则返回None。跟踪函数的返回值被忽略。 |
| exception | 此表示发生了异常。调用本地跟踪函数;arg是一个元组(exception、value、traceback)。返回值指定新的本地跟踪函数。 |
| c_call | 即将调用一个 C 函数。这可能是一个扩展函数或内置函数。arg是 C 函数对象。 |
| c_return | 一个 C 函数已经返回,arg是 C 函数对象。 |
| c_exception | C 函数引发了异常,arg是 C 函数对象。 |
正如您所预期的,通过一个简单的过滤函数,您可以轻松地确保只返回特定的函数,而不是通常得到的长列表。您真的不应该低估通过少量导入跟踪代码生成的数据量。前面的上下文管理器代码提供了 300 多行输出。
在第 10 章、测试和记录–为 Bug 做准备、我们关于测试和记录的章节中,我们了解了如何创建自定义记录器,为它们设置级别,并将处理程序添加到特定级别。我们现在将使用logging.DEBUG级别进行日志记录,这本身并没有什么特别之处,但是通过一些装饰程序,我们可以添加一些非常有用的只需调试的代码。
无论何时调试,我总是发现了解函数的输入和输出非常有用。带有装饰器的基本版本非常简单,可以编写;只需打印args和kwargs即可。下面的示例更进一步。通过使用inspect模块,我们还可以检索默认参数,从而可以在所有情况下显示带有参数名称和值的所有参数,即使未指定参数:
import pprint
import inspect
import logging
import functools
logging.basicConfig(level=logging.DEBUG)
def debug(function):
@functools.wraps(function)
def _debug(*args, **kwargs):
try:
result = function(*args, **kwargs)
finally:
# Extract the signature from the function
signature = inspect.signature(function)
# Fill the arguments
arguments = signature.bind(*args, **kwargs)
# NOTE: This only works for Python 3.5 and up!
arguments.apply_defaults()
logging.debug('%s(%s): %s' % (
function.__qualname__,
', '.join('%s=%r' % (k, v) for k, v in
arguments.arguments.items()),
pprint.pformat(result),
))
return _debug
@debug
def spam(a, b=123):
return 'some spam'
spam(1)
spam(1, 456)
spam(b=1, a=456)返回以下输出:
# python3 logged.py
DEBUG:root:spam(a=1, b=123): 'some spam'
DEBUG:root:spam(a=1, b=456): 'some spam'
DEBUG:root:spam(a=456, b=1): 'some spam'非常好的当然,因为我们清楚地看到何时调用函数,使用了哪些参数,以及返回了什么。但是,这可能只有在您积极调试代码时才能执行。通过添加特定于调试的记录器,您还可以使代码中的常规logging.debug语句变得更加有用,这将显示更多信息。只需将前面示例的日志记录配置替换为:
import logging
log_format = (
'[%(relativeCreated)d %(levelname)s] '
'%(pathname)s:%(lineno)d:%(funcName)s: %(message)s'
)
logging.basicConfig(level=logging.DEBUG, format=log_format)那么你的结果会是这样的:
# time python3 logged.py
[0 DEBUG] logged.py:31:_debug: spam(a=1, b=123): 'some spam'
[0 DEBUG] logged.py:31:_debug: spam(a=1, b=456): 'some spam'
[0 DEBUG] logged.py:31:_debug: spam(a=456, b=1): 'some spam'
python3 logged.py 0.04s user 0.01s system 96% cpu 0.048 total它显示相对于应用启动的时间(毫秒)和日志级别。随后是一个标识块,显示文件名、行号和生成日志的函数名。当然,最后还有一条信息。
当查看一段代码是如何运行的以及为什么运行时,查看整个堆栈跟踪通常很有用。当然,简单地提出一个例外是一种选择。然而,这将终止当前代码的执行,这通常不是我们要寻找的。这就是traceback模块派上用场的地方。只需几行简单的代码,我们就可以得到完整的(或有限的,如果您愿意)堆栈列表:
import traceback
class Spam(object):
def run(self):
print('Before stack print')
traceback.print_stack()
print('After stack print')
class Eggs(Spam):
pass
if __name__ == '__main__':
eggs = Eggs()
eggs.run()其结果如下:
# python3 traceback_test.py
Before stack print
File "traceback_test.py", line 18, in <module>
eggs.run()
File "traceback_test.py", line 8, in run
traceback.print_stack()
After stack print正如您所看到的,回溯只是简单地打印,没有任何异常。traceback模块实际上有很多其他方法用于打印基于异常等的回溯,但您可能不经常需要它们。最有用的可能是limit参数;此参数允许您将堆栈跟踪限制为有用的部分。例如,如果使用 decorator 或 helper 函数添加了此代码,则可能不需要在堆栈跟踪中包含这些代码。这就是limit参数的作用:
import traceback
class Spam(object):
def run(self):
print('Before stack print')
traceback.print_stack(limit=-1)
print('After stack print')
class Eggs(Spam):
pass
if __name__ == '__main__':
eggs = Eggs()
eggs.run()此导致如下:
# python3 traceback_test.py
Before stack print
File "traceback_test.py", line 18, in <module>
eggs.run()
After stack print如您所见,print_stack函数本身现在已从堆栈跟踪中隐藏,这使所有内容都变得更干净。
Python 3.5 中添加了负限制支持。在此之前,只支持正面限制。
asyncio模块有一些特殊的规定,使调试更加容易。鉴于asyncio中函数的异步性质,这是一个非常受欢迎的壮举。虽然调试多线程/多进程函数或类可能很困难,因为并发类可以轻松地与asyncio并行更改环境变量,但如果不是更多的话,也同样困难。
在大多数 Linux/Unix/Mac shell 会话中,可以使用它作为前缀来设置环境变量:
SOME_ENVIRONMENT_VARIABLE=value python3 script.py此外,可以使用export为当前 shell 会话配置:
export SOME_ENVIRONMENT_VARIABLE=value可以使用以下行获取当前值:
echo $SOME_ENVIRONMENT_VARIABLE在 Windows 上,您可以使用set命令为本地 shell 会话配置环境变量:
set SOME_ENVIRONMENT_VARIABLE=value可以使用以下行获取当前值:
set SOME_ENVIRONMENT_VARIABLE当使用PYTHONASYNCIODEBUG环境设置启用调试模式时,asyncio模块将检查每个定义的协同程序是否实际运行:
import asyncio
@asyncio.coroutine
def printer():
print('This is a coroutine')
printer()这会导致打印机协同程序出错,此处不会产生此错误:
# PYTHONASYNCIODEBUG=1 python3 asyncio_test.py
<CoroWrapper printer() running, defined at asyncio_test.py:4, created at asyncio_test.py:8> was never yielded from
Coroutine object created at (most recent call last):
File "asyncio_test.py", line 8, in <module>
printer()此外,event循环默认有一些日志消息:
import asyncio
import logging
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()这将导致调试消息中出现,例如如下所示:
# PYTHONASYNCIODEBUG=1 python3 asyncio_test.py
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:asyncio:Close <_UnixSelectorEventLoop running=False closed=False debug=True>您可能想知道为什么我们使用PYTHONASYNCIODEBUG标志而不是loop.set_debug(True)。原因是,在某些情况下,由于启用调试太晚,这将不起作用。例如,在使用前面的printer()时,您会发现单独使用loop.set_debug(True)不会出现任何错误。
启用调试时,以下内容将更改:
- 尚未生成的协程(如前几行所示)将引发异常。
- 从“错误”线程调用协同路由会引发异常。
- 将记录选择器的执行时间。
- 将记录慢速回调(超过 100 毫秒)。此超时可通过
loop.slow_callback_duration修改。 - 资源未正确关闭时将发出警告。
- 将记录在执行之前销毁的任务。
faulthandler模块有助于调试真正的低级别崩溃,也就是说,只有在使用低级别内存访问(如 C 扩展)时才可能发生崩溃。
例如,下面是一些会导致 Python 解释器崩溃的代码:
import ctypes
# Get memory address 0, your kernel shouldn't allow this:
ctypes.string_at(0)其结果与以下类似:
# python faulthandler_test.py
zsh: segmentation fault python faulthandler_test.py当然,这是一个相当丑陋的回答,你不可能处理这个错误。万一你想知道,在这些情况下,拥有一个try/except结构也帮不了你。以下代码将以完全相同的方式崩溃:
import ctypes
try:
# Get memory address 0, your kernel shouldn't allow this:
ctypes.string_at(0)
except Exception as e:
print('Got exception:', e)这就是faulthandler模块帮助的地方。它仍然会导致解释器崩溃,但至少您会看到一条正确的错误消息,因此,如果您(或任何子库)与原始内存有任何交互,这是一个良好的默认设置:
import ctypes
import faulthandler
faulthandler.enable()
# Get memory address 0, your kernel shouldn't allow this:
ctypes.string_at(0)其结果大致如下:
# python faulthandler_test.py
Fatal Python error: Segmentation fault
Current thread 0x00007fff79171300 (most recent call first):
File "ctypes/__init__.py", line 491 in string_at
File "faulthandler_test.py", line 7 in <module>
zsh: segmentation fault python faulthandler_test.py显然,以这种方式退出 Python 应用是不可取的,因为代码不会以正常的清理方式退出。资源不会完全关闭,也不会调用退出处理程序。如果您需要捕获这种行为,那么最好将 Python 可执行文件包装在一个单独的脚本中。
既然我们已经讨论了将始终有效的基本调试方法,那么我们将看看交互式调试,了解一些更高级的调试技术。以前的调试方法通过修改代码和/或预见使变量和堆栈可见。这一次,我们将看一个稍微聪明一点的方法,它包括交互地做同样的事情,但一旦需要时。
在测试一些 Python 代码时,您可能已经多次使用交互控制台,因为它是测试 Python 代码的一个简单而有效的工具。您可能不知道的是,从代码中启动自己的 shell 实际上很简单。因此,无论何时,只要您想从代码中的特定点进入常规 shell,都很容易做到:
import code
def spam():
eggs = 123
print('The begin of spam')
code.interact(banner='', local=locals())
print('The end of spam')
print('The value of eggs: %s' % eggs)
if __name__ == '__main__':
spam()执行此操作时,我们将中途进入交互式控制台:
# python3 test_code.py
The begin of spam
>>> eggs
123
>>> eggs = 456
>>>
The end of spam
The value of eggs: 123要退出此控制台,我们可以在 Linux/Mac 系统上使用*^d*(Ctrl+d),在 Windows 系统上使用*^z*(Ctrl+z)。
这里需要注意的一个重要的点是,范围在两者之间不共享。尽管为了方便起见,我们通过locals()来共享局部变量,但这种关系不是双向的。结果是,即使我们在交互会话中将eggs设置为456,它也不会转移到外部功能。如果愿意,可以通过直接操作(例如,设置属性)修改外部范围中的变量,但所有在本地声明的变量都将保持在本地。
当涉及到实际调试代码时,常规的交互控制台并不适合。只要稍加努力,您就可以使它工作,但调试起来并不那么方便,因为您只能看到当前的作用域,不能轻松地跳转堆栈。使用pdb(Python 调试器),这很容易实现。让我们来看一个使用pdb的简单示例:
import pdb
def spam():
eggs = 123
print('The begin of spam')
pdb.set_trace()
print('The end of spam')
print('The value of eggs: %s' % eggs)
if __name__ == '__main__':
spam()这个例子与上一段中的例子几乎相同,只是这次我们使用的是pdb控制台,而不是常规的交互式控制台。让我们尝试一下交互式调试器:
# python3 test_pdb.py
The begin of spam
> test_pdb.py(8)spam()
-> print('The end of spam')
(Pdb) eggs
123
(Pdb) eggs = 456
(Pdb) continue
The end of spam
The value of eggs: 456正如你所看到的,我们实际上已经修改了eggs的值。在这个例子中,我们使用了完整的continue命令,但是所有的pdb命令都有简短的版本。因此,使用c而不是continue会得到相同的结果。只需键入eggs(或任何其他变量)即可显示内容,设置变量只需设置即可,就像我们在交互式会话中所期望的那样。
要开始使用pdb,首先,这里显示了最有用(完整)的速记命令列表:
命令
|
解释
|
| --- | --- |
| h(elp) | 此显示命令列表(此列表)。 |
| h(elp) command | 此显示给定命令的帮助。 |
| w(here) | 当前在当前帧上带有箭头的堆栈跟踪。 |
| d(own) | 将向下/移动到堆栈中较新的帧。 |
| u(p) | 将向上/移动到堆栈中较旧的帧。 |
| s(tep) | 执行当前行并尽快停止。 |
| n(ext) | 执行当前行,并在当前功能的下一行停止。 |
| r(eturn) | 继续执行,直到函数返回。 |
| c(ont(inue)) | 继续执行直到下一个断点。 |
| l(ist) [first[, last]] | 列出当前行周围的行源代码(默认为 11 行)。 |
| ll | longlist | 列出当前函数或帧的所有源代码。 |
| source expression | 列出给定对象的源代码。这类似于长列表。 |
| a(rgs) | 打印当前函数的参数。 |
| pp expression | 漂亮的印刷体给定的表达式。 |
| b(reak) | 显示断点列表。 |
| b(reak) [filename:]lineno | 将断点放置在给定行号处,也可以选择文件。 |
| b(reak) function[, condition] | 在给定函数处放置一个断点。该条件是一个表达式,其计算结果必须为True,断点才能工作。 |
| cl(ear) [filename:]lineno | 清除此行的断点。 |
| cl(ear) breakpoint [breakpoint ...] | 用这些数字清除断点。 |
| Command | 列出所有定义的命令。 |
| command breakpoint | 指定在遇到给定断点时要执行的命令列表。使用end命令结束列表。 |
| Alias | 列出所有别名。 |
| alias name command | 创建一个别名。该命令可以是任何有效的 Python 表达式,因此您可以执行以下操作来打印对象的所有属性:
alias pd pp %1.__dict__ |
| unalias name | 删除别名。 |
| ! statement | 在堆栈中的当前点执行语句。通常不需要!符号,但如果与调试器命令发生冲突,这可能会很有用。例如,尝试b = 123。 |
| Interact | 打开与上一段类似的互动会话。请注意,在该局部范围内设置的变量将不会被传输。 |
这是一个相当长的列表,但是你可能会经常使用其中的大部分。为了突出显示上表中显示的选项之一,让我们演示断点的设置和使用:
import pdb
def spam():
print('The begin of spam')
print('The end of spam')
if __name__ == '__main__':
pdb.set_trace()
spam()到目前为止,没有发生任何新的情况,但现在让我们打开交互式调试会话,如下所示:
# python3 test_pdb.py
> test_pdb.py(11)<module>()
-> while True:
(Pdb) source spam # View the source of spam
4 def spam():
5 print('The begin of spam')
6 print('The end of spam')
(Pdb) b 5 # Add a breakpoint to line 5
Breakpoint 1 at test_pdb.py:5
(Pdb) w # Where shows the current line
> test_pdb.py(11)<module>()
-> while True:
(Pdb) c # Continue (until the next breakpoint or exception)
> test_pdb.py(5)spam()
-> print('The begin of spam')
(Pdb) w # Where again
test_pdb.py(12)<module>()
-> spam()
> test_pdb.py(5)spam()
-> print('The begin of spam')
(Pdb) ll # List the lines of the current function
4 def spam():
5 B-> print('The begin of spam')
6 print('The end of spam')
(Pdb) b # Show the breakpoints
Num Type Disp Enb Where
1 breakpoint keep yes at test_pdb.py:5
breakpoint already hit 1 time
(Pdb) cl 1 # Clear breakpoint 1
Deleted breakpoint 1 at test_pdb.py:5这是一个大量的输出,但实际上并不像看起来那么复杂:
- 首先,我们使用
source spam命令查看spam函数的源代码。 - 之后,我们知道了第一条
print语句的行号,我们使用它在第 5 行放置断点(b 5。 - 为了检查我们是否仍然在正确的位置,我们使用了
w命令。 - 由于设置了断点,我们使用
c继续到下一个断点。 - 在第 5 行的断点处停下来后,我们再次使用
w来确认这一点。 - 使用
ll列出当前函数的代码。 - 使用
b列出断点。 - 使用带有上一个命令中的断点编号的
cl 1再次删除断点。
一开始这一切看起来有点复杂,但您会发现,一旦您尝试了几次,它实际上是一种非常方便的调试方式。
为了更好,这次我们只在eggs = 3时执行断点。代码基本相同,但在本例中我们需要一个变量:
import pdb
def spam(eggs):
print('eggs:', eggs)
if __name__ == '__main__':
pdb.set_trace()
for i in range(5):
spam(i)现在,让我们执行代码,确保它只在特定时间中断:
# python3 test_breakpoint.py
> test_breakpoint.py(10)<module>()
-> for i in range(5):
(Pdb) source spam
4 def spam(eggs):
5 print('eggs:', eggs)
(Pdb) b 5, eggs == 3 # Add a breakpoint to line 5 whenever eggs=3
Breakpoint 1 at test_breakpoint.py:5
(Pdb) c # Continue
eggs: 0
eggs: 1
eggs: 2
> test_breakpoint.py(5)spam()
-> print('eggs:', eggs)
(Pdb) a # Show function arguments
eggs = 3
(Pdb) c # Continue
eggs: 3
eggs: 4要列出我们所做的工作:
- 首先,使用
source垃圾邮件,我们查找行号。 - 之后,我们放置了一个带有
eggs == 3条件的断点。 - 然后我们使用
c继续执行。如您所见,0、1和2的值打印正常。 - 在值
3处达到断点。为了验证这一点,我们使用a查看函数参数。 - 我们继续执行代码的其余部分。
所有这些都是对pdb.set_trace()函数的手动调用,但通常情况下,您只是在运行应用,并不真正期待问题。这就是异常捕获非常方便的地方。除了自己导入pdb之外,您还可以通过pdb作为一个模块运行脚本。让我们检查一下这段代码,它一达到零除法就消失了:
print('This still works')
1/0
print('We shouldnt reach this code')如果我们使用pdb参数运行它,那么每当它崩溃时,我们都可以在 Python 调试器中结束:
# python3 -m pdb test_zero.py
> test_zero.py(1)<module>()
-> print('This still works')
(Pdb) w # Where
bdb.py(431)run()
-> exec(cmd, globals, locals)
<string>(1)<module>()
> test_zero.py(1)<module>()
-> print('This still works')
(Pdb) s # Step into the next statement
This still works
> test_zero.py(2)<module>()
-> 1/0
(Pdb) c # Continue
Traceback (most recent call last):
File "pdb.py", line 1661, in main
pdb._runscript(mainpyfile)
File "pdb.py", line 1542, in _runscript
self.run(statement)
File "bdb.py", line 431, in run
exec(cmd, globals, locals)
File "<string>", line 1, in <module>
File "test_zero.py", line 2, in <module>
1/0
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> test_zero.py(2)<module>()
-> 1/0pdb中一个有用的小技巧是使用回车按钮,默认情况下,该按钮将再次执行之前执行的命令。这在单步执行程序时非常有用。
commands命令有点复杂,但非常有用。它允许您在遇到特定断点时执行命令。为了说明这一点,让我们再次从一个简单的例子开始:
import pdb
def spam(eggs):
print('eggs:', eggs)
if __name__ == '__main__':
pdb.set_trace()
for i in range(5):
spam(i)代码非常简单,因此现在我们将添加断点和命令,如下所示:
# python3 test_breakpoint.py
> test_breakpoint.py(10)<module>()
-> for i in range(3):
(Pdb) b spam # Add a breakpoint to function spam
Breakpoint 1 at test_breakpoint.py:4
(Pdb) commands 1 # Add a command to breakpoint 1
(com) print('The value of eggs: %s' % eggs)
(com) end # End the entering of the commands
(Pdb) c # Continue
The value of eggs: 0
> test_breakpoint.py(5)spam()
-> print('eggs:', eggs)
(Pdb) c # Continue
eggs: 0
The value of eggs: 1
> test_breakpoint.py(5)spam()
-> print('eggs:', eggs)
(Pdb) cl 1 # Clear breakpoint 1
Deleted breakpoint 1 at test_breakpoint.py:4
(Pdb) c # Continue
eggs: 1
eggs: 2正如您所看到的,我们可以很容易地向断点添加命令。删除断点后,这些命令显然将不再执行。
虽然通用 Python 控制台很有用,但它的边缘可能有点粗糙。IPython 控制台提供了全新的额外功能,这使它成为一个更好的控制台。其中一个特性是更方便的调试器。
首先,确保已安装ipdb:
pip install ipdb接下来,让我们使用前面的脚本再次尝试调试器。唯一的小变化是我们现在导入的是ipdb而不是pdb:
import ipdb
def spam(eggs):
print('eggs:', eggs)
if __name__ == '__main__':
ipdb.set_trace()
for i in range(3):
spam(i)然后我们执行它:
# python3 test_ipdb.py
> test_ipdb.py(10)<module>()
9 ipdb.set_trace()
---> 10 for i in range(3):
11 spam(i)
ipdb> b spam # Set a breakpoint
Breakpoint 1 at test_ipdb.py:4
ipdb> c # Continue (until exception or breakpoint)
> test_ipdb.py(5)spam()
1 4 def spam(eggs):
----> 5 print('eggs:', eggs)
6
ipdb> a # Show the arguments
eggs = 0
ipdb> c # Continue
eggs: 0
> test_ipdb.py(5)spam()
1 4 def spam(eggs):
----> 5 print('eggs:', eggs)
6
ipdb> # Repeat the previous command, so continue again
eggs: 1
> test_ipdb.py(5)spam()
1 4 def spam(eggs):
----> 5 print('eggs:', eggs)
6
ipdb> cl 1 # Remove breakpoint 1
Deleted breakpoint 1 at test_ipdb.py:4
ipdb> c # Continue
eggs: 2命令都是一样的,但在我看来,输出只是更清晰了一点。实际版本还包括语法突出显示,这使得输出更容易理解。
简言之,在大多数情况下,您只需将pdb替换为ipdb,即可获得更直观的调试器。但我也会向ipdb上下文管理器推荐:
import ipdb
with ipdb.launch_ipdb_on_exception():
main()这看起来很方便。它只是将ipdb挂接到您的异常中,以便您可以在需要时轻松地进行调试。将其与调试标志结合到应用中,以便在需要时方便地允许调试。
pdb和ipdb只是 Python 可用的大量调试器中的两个。目前值得注意的一些调试器如下:
pudb:此提供全屏命令行调试器pdbpp:将勾入常规pdbrpdb2:这是一个远程调试器,允许将挂接到正在运行的(远程)应用中Werkzeug:这是一个基于 web 的调试器,允许在 web 应用运行时进行调试
当然还有很多其他的,没有一个是绝对最好的。与所有工具一样,它们都有各自的优点和缺点,只有您才能正确地决定最适合您当前目的的工具。您当前的 Python IDE 可能已经有了一个集成的调试器。
除了在遇到问题时进行调试外,有时您只需跟踪错误以便以后进行调试。尤其是在使用远程服务器时,这些对于检测 Python 进程何时以及如何发生故障非常有用。此外,这些服务还提供了错误分组,这使得它们比一个简单的电子邮件异常类型的脚本更有用,它可以快速地向收件箱发送垃圾邮件。
一个很好的跟踪错误的开源解决方案是sentry。如果您需要提供性能跟踪的完整 Fletch 解决方案,那么 Opbeat 和 Newrelic 是非常好的解决方案;他们提供免费和付费版本。请注意,所有这些还支持跟踪其他语言,例如 JavaScript。
本章解释了几种不同的调试技术和要点。当然,关于调试还有很多可以说的,但我希望您现在已经获得了调试 Python 代码的有利位置。交互式调试技术对于单线程应用和交互式会话可用的位置非常有用。但由于情况并非总是如此,我们还讨论了一些非交互式选项。
以下是本章讨论的所有要点的概述:
- 非交互式调试使用:
printloggingtracetracebackasynciofaulthandler
- 使用
pdb和ipdb进行交互调试
在下一章中,我们将看到如何监视和改进 CPU 和内存性能,以及如何查找和修复内存泄漏。