调试指南

关于本指南的演示,请查看 Antonio Kam 的 视频

简介

当 Python 程序发生错误时,会显示回溯信息。例如:

Traceback (most recent call last):
  File ".../ex.py", line 7, in <module>
    print(f(4))
          ^^^^
  File ".../ex.py", line 2, in f
    print(g(x + 1) + 2)
          ^^^^^^^^
  File ".../ex.py", line 5, in g
    return print(2) + 3
           ~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

回溯信息

回溯中的行是成对出现的。第一行每对具有以下格式:

File "<file name>", line <number>, in <function>

该行为你提供以下信息:

  • 文件名:包含问题的文件名。
  • 行号:导致问题的文件中的行号,或包含下一个函数调用的行号
  • 函数:可以找到该行的函数名称。

配对中的第二行(它比第一行缩进更多)显示进行下一个函数调用的实际代码行。这让你快速查看涉及了哪些表达式。

回溯的组织方式是最近的调用在最后,所以请查看底部。

错误信息

回溯信息中的最后一行是错误语句。错误语句具有以下格式:

<error type>: <error message>

这一行为你提供两条信息:

  • 错误类型:导致的错误类型(例如 SyntaxErrorTypeError)。这些通常足够描述性,可以帮助你缩小搜索错误原因的范围。
  • 错误信息:关于到底是什么导致了错误的更详细描述。不同的错误类型产生不同的错误信息。

调试技巧

运行文档测试

Python 有一种很好的方法可以快速为你的代码编写测试。这些称为文档测试(doctest),看起来像这样:

def foo(x):
    """一个随机函数。

    >>> foo(4)
    4
    >>> foo(5)
    5
    """

文档字符串中看起来像解释器输出的行就是文档测试。要运行它们,请转到终端并输入:

python3 -m doctest file.py

这会将你的文件加载到 Python 解释器中,并检查每个文档测试输入(例如 foo(4))是否与指定的输出(例如 4)相同。如果不是,一条消息会告诉你可能失败的文档测试。

命令行工具有一个 -v 选项,代表详细

python3 -m doctest file.py -v

除了告诉你可能失败的文档测试外,它还会告诉你可能通过的文档测试。

通常,我们会在起始文件中为你提供文档测试。你可以通过遵循相同格式添加更多测试。除了帮助程序本身的实现之外,编写额外的测试以揭示有关输入形状和问题预期输出的更多细节通常很有帮助。预先花一点时间编写测试可以节省以后大量时间。

打印输出

一旦文档测试告诉你在哪里出错,你就必须弄清楚出了什么问题。如果文档测试给了你回溯信息,请查看它是什么类型的错误以帮助缩小搜索范围。还要检查你是否没有犯任何常见错误

当你第一次学习如何编程时,可能很难发现代码中的错误。一种常见的做法是添加 print 调用。例如,假设以下函数 foo 一直返回错误的结果:

def foo(x):
    result = some_function(x)
    return result // 5

我们可以在返回之前添加一个打印调用来检查 some_function 返回了什么:

def foo(x):
    result = some_function(x)
    print('DEBUG: result is', result)
    return other_function(result)

注意:用特定的字符串 "DEBUG: " 作为调试语句的前缀可以让 cs61a 使用的 ok 自动评分器忽略它们。 由于 ok 通常测试你代码的所有输出,如果有没有明确标记为这样的调试语句,即使输出相同,它也会失败。

如果结果不是我们期望的,我们会去 some_function 中查看它是否正常工作。否则,我们可能不得不在返回之前添加一个打印调用来检查 other_function

def foo(x):
    result = some_function(x)
    print('DEBUG: result is', result)
    tmp = other_function(result)
    print('DEBUG: other_function returns', tmp)
    return tmp

一些建议:

  • 不要只打印一个变量——添加某种消息让你更容易阅读:

    print(x)   # 更难解释
    print('DEBUG: x =', x)  # 更容易
  • 使用 print 调用查看函数调用的结果(即在函数调用之后)。
  • while 循环结束时使用 print 调用,以查看每次迭代后计数器变量的状态:

    i = 0
    while i < n:
        i += func(i)
        print('DEBUG: i is', i)

交互式调试

许多程序员喜欢的一种调查代码的方式是交互式地使用 Python:

python3 -i file.py

在执行 file.py 的内容后启动一个交互式 Python 会话。

如果你使用的是 ok 自动评分器,-i 选项会在失败测试用例的环境中启动一个交互式会话:

python3 ok -q ### Q1: name -i

然后你可以评估与测试相关的表达式,看看哪里出错了。

PythonTutor 调试

有时理解给定 Python 代码片段的最佳方法是创建一个环境图。PythonTutor 自动创建环境图。

错误类型

以下是 Python 程序员遇到的常见错误类型。

SyntaxError

  • 原因:代码语法错误
  • 示例

      File "file name", line number
        def incorrect(f)
                        ^
    SyntaxError: invalid syntax
  • 解决方案^ 符号指向包含无效语法的代码。错误消息不会告诉你什么出错了,但它会告诉你在哪里
  • 注意:Python 将在执行任何代码之前检查 SyntaxError。这与其他错误不同,其他错误只在运行时引发。

IndentationError

  • 原因:缩进不正确
  • 示例

      File "file name", line number
        print('improper indentation')
    IndentationError: unindent does not match any outer indentation level
  • 解决方案:显示缩进不正确的行。只需重新缩进它。
  • 注意:如果你在制表符和空格之间不一致,Python 会引发其中之一。确保你使用空格!(在 Python 中使用空格通常更不容易头疼,所有 cs61a 内容都使用空格)。

TypeError

  • 原因 1

    • 原始运算符的操作数类型无效。你可能正在尝试对不兼容的类型进行加/减/乘/除。
    • 示例

      TypeError: unsupported operand type(s) for +: 'function' and 'int'
  • 原因 2

    • 在函数调用中使用非函数对象。
    • 示例

      >>> square = 3
      >>> square(3)
      Traceback (most recent call last):
        ...
      TypeError: 'int' object is not callable
  • 原因 3

    • 向函数传递不正确数量的参数。
    • 示例

      >>> add(3)
      Traceback (most recent call last):
        ...
      TypeError: add expected 2 arguments, got 1

NameError

  • 原因:变量未分配给任何东西或者它不 存在。这包括函数名称。
  • 示例

    File "file name", line number
      y = x + 3
    NameError: global name 'x' is not defined
  • 解决方案:确保你在使用变量之前初始化它(即将变量分配给一个值)。
  • 注意:错误消息说"global name"的原因是 Python 将从函数的本地帧开始搜索变量。如果在那里找不到变量,Python 将继续搜索父帧,直到到达全局帧。如果仍然找不到变量,Python 会引发错误。

IndexError

  • 原因:尝试用超过序列大小的数字索引序列(例如元组、列表、字符串)。
  • 示例

    File "file name", line number
      x[100]
    IndexError: tuple index out of range
  • 解决方案:确保索引在序列的范围内。如果你使用变量作为索引(例如 seq[x]),请确保变量被分配给正确的索引。

常见错误

拼写

Python 区分大小写。变量 helloHellohallohelo 不相同。这通常表现为 NameError,但有时拼写错误的变量实际上已经被定义了。在这种情况下,可能很难找到错误,而且发现只是拼写错误从来都不会让人满意。

缺少括号

一个常见的错误是漏掉右括号。这将表现为 SyntaxError。考虑以下代码:

def fun():
    return foo(bar()   # 这里缺少一个括号

fun()

Python 将引发 SyntaxError,但会指向缺少括号之后的行:

File "file name", line "number"
    fun()
      ^
SyntaxError: invalid syntax

通常,如果 Python 将 SyntaxError 指向一个看似正确的行,你可能在某些地方忘记了括号。

缺少闭合引号

这与之前的错误类似,但更容易捕获。Python 实际上会告诉你是哪一行缺少引号:

File "file name", line "number"
  return 'hi
           ^
SyntaxError: EOL while scanning string literal

EOL 代表"行尾"。

= vs. ==

单个等号 = 用于赋值;双等号 == 用于测试等价性。这种形式最常见的错误是这样的:

if x = 3:

无限循环

无限循环通常由条件永远不变的 while 循环引起。例如:

i = 0
while i < 10:
    print(i)

有时你可能递增了错误的计数器:

i, n = 0, 0
while i < 10:
    print(i)
    n += 1

差一错误

有时 while 循环或递归函数可能会提前一次迭代停止。在这里,最好是逐步检查迭代/递归,看看循环在哪个数字停止。