介绍
到现在为止,您在这个课程编程时已经遇到了各种错误。大多数情况下,您会尝试运行代码并看到如下内容:
Traceback (most recent call last):
File "<pyshell#29>", line 3 in <module>
result = buggy(5)
File <pyshell#29>", line 5 in buggy
return f + x
TypeError: unsupported operand type(s) for +: 'function' and 'int'
这称为追踪信息。它打印出导致错误的函数调用链,最近调用的函数在底部。您可以按照此链找出导致问题的函数。
追踪信息
请注意,追踪信息的行看起来是成对的。这一对中的第一行具有以下格式:
File "<file name>", line <number>, in <function>
该行给您提供以下信息:
- 文件名:包含问题的文件名。
- 数字 : 文件中导致问题的行号,或包含下一个函数调用的行号
- 函数:可以在其中找到该行的函数的名称。
该对中的第二行(比第一行缩进得更远)显示进行下一个函数调用的实际代码行。这使您可以快速查看传递给函数的参数,函数被使用的环境等。
最后,请记住,追踪信息是用“最近的调用在最后”来组织的。
错误信息
追踪信息中的最后一行是错误语句。一个错误的语句格式如下:
<error type>: <error message>
这一行给您提供了两条信息:
- 错误类型:导致的错误类型(例如
SyntaxError
,TypeError
)。这些通常具有足够的描述,可以帮助您缩小对错误原因的搜索范围。 - 错误消息:对导致错误的确切原因的更详细描述。不同的错误类型会产生不同的错误消息。
调试技巧
运行文档测试
Python 有一种很好的方法可以为您的代码快速编写测试。这些被称为 doctests,看起来像这样:
def foo(x):
"""A random function.
>>> foo(4)
4
>>> foo(5)
5
"""
docstring 中看起来像解释器输出的行是 doctests。要运行它们,请转到您的终端并键入:
python3 -m doctest file.py
这将有效地将您的文件加载到 Python 解释器中,并检查每个 doctest 输入(例如foo(4)
)是否与指定的输出(例如4
)相同。如果不是,一条消息会告诉你哪些 doctests 失败了。
命令行工具有一个-v
选项来输出详细信息。
python3 -m doctest file.py -v
除了告诉你哪些 doctests 你失败了,它还会告诉你哪些 doctests 通过了。
通常,我们会在启动文件中为您提供 doctests。您可以按照相同的格式添加更多测试。除了可以帮助程序本身的实现,编写额外的测试来揭示关于输入的状况和问题的期望输出的更多细节通常也很有帮助。花一点时间预先编写测试可以节省大量时间。
编写自己的测试
除了 doctests,您还可以编写自己的测试。有两种方法可以做到这一点:(1) 编写额外的 doctests (2) 编写测试函数。
要编写更多 doctests,只需遵循现有 doctests 的风格。您还可以编写自己的函数(很像项目 1 中的take_turn_test
函数)。
编写测试的一些建议:
- 在编写代码之前先写一些测试:这称为测试驱动开发。首先写下您期望的函数行为方式—这可以指导您编写实际代码。
- 编写代码之后写更多测试:一旦您确定您的代码通过了初始的 doctests,请编写更多测试以处理边界情况。
- 测试边界情况:确保您的代码适用于所有特殊情况。
使用print
语句
一旦 doctests 告诉你错误在哪,你就必须弄清楚哪里出了问题。如果 doctest 给了追踪信息,请查看它是什么 错误类型以帮助缩小搜索范围。还要检查您没有犯任何常见错误。
当您第一次学习如何编程时,很难发现代码中的错误。一种常见的做法是添加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
通常会测试代码的所有输出,因此如果存在未明确标记为此类的调试语句,即使输出相同,它也会失败。
如果结果显示result
不是我们期望的那样,我们会去看看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(tmp) # harder to keep track print('DEBUG: tmp was this:', tmp) # easier
-
使用
print
语句查看函数调用的结果(即在函数调用之后)。 -
在
while
循环末尾使用print
语句来查看每次迭代后计数器变量的状态:i = 0 while i < n: i += func(i) print('DEBUG: i is', i)
-
不要只是在明显正确的行之后随机放置
print
语句。
长期调试
上述print
语句用于快速调试一次性错误——找出错误后,您将删除所有print
语句。
但是,如果我们需要定期测试我们的文件,有时我们会希望留下调试代码。如果每次运行我们的文件时,都会弹出调试消息,这可能会有点烦人。避免这种情况的一种方法是使用全局debug
变量:
debug = True
def foo(n):
i = 0
while i < n:
i += func(i)
if debug:
print('DEBUG: i is', i)
现在,每当我们想做一些调试时,我们可以将全局变量debug
设置 为True
,而当我们不想看到任何调试输入时,我们可以将其设置为False
(这样的变量称为“标志”)。
交互式调试
许多程序员喜欢研究他们的代码的一种方法是使用交互式 REPL。也就是您可以直接运行函数并检查其输出的终端。
通常,要完成此操作,您可以运行
python -i file.py
然后有一个 python 会话,其中file.py
所有的定义都已经被执行。
如果您使用ok
自动评分器,它有一个特定的工具,可以让您跳到失败的测试用例中间。只需运行
python ok -q ### Q1: name -i
如果该问题有测试用例失败,则启动代码和 doctest 将被打印在屏幕上并运行,然后您将可以访问终端,在其中执行与程序相关的命令。
PythonTutor调试
有时,了解给定的 Python 代码段发生了什么的最好方法是创建一个环境图。虽然手动创建环境图有时会很乏味,但PythonTutor可以自动创建环境图。要使用此工具,您可以将代码复制粘贴到其中,然后运行它。或者,如果您正在使用ok
自动评分器做作业,则可以运行
python ok -q ### Q2: name --trace
应该会打开一个带有你的代码的浏览器窗口。
使用assert
断言语句
Python 有一个称为assert
断言语句的功能,它允许您测试条件是否为真,否则在一行中打印错误消息。如果您知道在程序中的某些点某些条件需要为真,这将非常有用。例如,如果您正在编写一个接受整数并将其加倍的函数,确保您的输入确实是一个整数可能会很有用。然后就可以编写如下代码
def double(x):
assert isinstance(x, int), "The input to double(x) must be an integer"
return 2 * x
请注意,我们并不是在这里真正调试double
函数,我们所做的是确保调用double
的任何人 都使用正确的参数。例如,如果我们有一个函数g
,它接受一个字符串和一个数字,并让字符串的长度加数字的两倍,它是这样实现的:
def g(x, y):
return double(x) + y # should be double(y) + len(x)
我们没有得到一个关于无法将字符串和数字相加的奇怪错误,而是一个清晰的错误,即double
的参数必须是整数。这使我们能够快速缩小问题的范围。
assert 断言语句的一个主要好处是它们不仅仅是一种调试工具,您可以将它们永久地保留在代码中。软件开发中的一个关键原则是,代码崩溃通常比产生不正确的结果要好,并且在代码中使用断言使得如果代码中有错误,他就更有可能崩溃。
错误类型
以下是 Python 程序员遇到的常见错误类型。
SyntaxError
-
原因:代码语法错误
-
示例:
File "file name", line number def incorrect(f) ^ SyntaxError: invalid syntax
-
解决方案:
^
符号指向包含无效语法的代码。错误消息不会告诉您出了什么问题,但会告诉您在哪里出了问题。 -
注意:Python 会在执行任何代码之前进行检查是否有
SyntaxErrors
。这与其他错误不同,后者仅在运行时产生。
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
-
解决方案:确保在使用变量之前初始化变量(即为变量赋值)。
-
注意:错误消息显示“全局名称”的原因是 Python 一开始从函数的本地帧搜索变量。如果在那里找不到该变量,Python 将继续搜索父帧,直到它到达全局帧。如果仍然找不到变量,Python 会引发该错误。
IndexError
-
原因:试图用超过序列大小的数字对序列(例如元组、列表、字符串)进行索引。
-
示例:
File "file name", line number x[100] IndexError: tuple index out of range
-
解决方案:确保索引在序列范围内。如果您使用变量作为索引(例如
seq[x]
,确保变量被赋值为正确的索引。
常见错误
拼写
Python 区分大小写。变量hello
和Hello
、hel1o
或helo
是不一样的。这通常会显示为NameError
错误,但有时实际上已经定义了拼写错误的变量。在这种情况下,可能很难发现错误,而且发现这只是拼写错误永远不会令人欣慰。
缺少括号
一个常见的错误是省略了右括号。这将显示为 SyntaxError
. 考虑以下代码:
def fun():
return foo(bar() # missing a parenthesis here
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
代表“行尾”。
=
与 ==
单个等于号=
用于赋值;双等于号==
用于测试等价性。这种形式最常见的错误是这样的:
if x = 3:
无限循环
无限循环通常是由while
循环条件永不改变引起的。例如:
i = 0
while i < 10:
print(i)
有时您可能增加了错误的计数器:
i, n = 0, 0
while i < 10:
print(i)
n += 1
差一错误
有时,while
循环或递归函数可能会在很短的时间内停止但是差或者多一次迭代。这时,最好遍历迭代/递归以查看循环停止在哪个数上。
留言