Debugging调试教程(来自CS61A)&&freeCodeCamp

调试|CS 61A 2020 年秋季 (berkeley.edu)

介绍

到目前为止,您在完成这门课的编程任务时,已经遇到各种错误。大多数情况下,您尝试运行代码并看见像下面这样的消息:

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'

这称为回溯(traceback)消息。它打印出导致错误的函数调用链,在底部调用最新的函数。您可以按照此链来找出哪个函数导致了问题。

回溯(Traceback)消息

请注意,Traceback中的行似乎配对在一起。 第一行具有以下格式的配对:

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

该行 为您提供以下信息:

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

该配对中的第二行(相比第一行缩进更多) 显示生成导致下一个函数被调用的实际代码行。这使您可以快速查看传递到的函数,以及在怎样的上下文中函数被使用等。

最后,请记住,traceback是用最近或最后调用函数。【意味着可能前面也存在错误,但这里只返回了最后出现错误的函数以及函数链,可以从后往前排查错误】

错误消息

traceback消息的最后一行是错误语句。错误语句具有以下格式:

<error type>: <error message>

此行为您提供两条信息:

  • 错误类型:导致的错误类型(例如 中英文 逗号,即不能用中文逗号代替英文逗号)。这些通常具有足够的描述性,从而能帮助您缩小搜索范围,查找错误原因。SyntaxErrorTypeError
  • 错误消息:更详细地描述确切的内容 导致错误。不同的错误类型产生不同的错误 消息。

调试技术

运行文档测试

Python 有一个很好的方法来快速为你的代码编写测试。这些是 称为文档测试,看起来像这样:

def foo(x):
    """A random function.

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

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

python3 -m doctest file.py

这有效地将您的文件加载到 Python 解释器中,并且检查每个 doctest 输入是否是指定的输出。如果不是,则会显示一条消息告诉您,哪些文档测试失败了。foo(4)4

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

python3 -m doctest file.py -v

除了告诉你哪些文档测试失败之外,它还会告诉您哪些文档测试通过了。

通常,我们会在入门文件中为您提供文档测试。你可以按照相同的格式添加更多测试。

编写其他测试以发现某些输入和预期输出的具体细节通常是有帮助的,也可以帮助完成program。前期花在编写测试上一点时间可以节省大量时间。

编写自己的测试

除了文档测试之外,您还可以编写自己的测试。有两个执行此操作的方法:(1)编写额外的文档测试(2)编写测试功能。

要编写更多文档测试,只需遵循现有文档测试的风格即可。 您还可以编写自己的函数(非常类似于项目 1 中的函数)。take_turn_test

写作测试中的一些建议:

  • 在编写代码之前编写一些测试:这称为测试驱动开发。写下您希望函数如何首先表现 —— 这可以指导您编写实际代码。
  • 编写代码后编写更多测试:一旦确定您的代码通过了最初的文档测试,编写更多的测试来考虑的边缘或极端情况。
  • 测试边缘或极端情况:确保您的代码适用于所有特殊情况。

使用语句print

一旦文档测试告诉您错误在哪里,您必须弄清楚哪里出错了。如果 doctest 为您提供了回溯消息,请查看它是什么类型的错误,以帮助缩小搜索范围。也检查您是否没有犯任何常见错误

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

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

我们可以在返回之前添加一个 print 语句来检查返回的内容:some_function

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

注意:在调试语句前面加上特定字符串后缀允许 cs61a 使用的自动评分器忽略它们。 由于通常会测试代码的所有输出,因此 如果存在未显式的调试语句,将失败 这样标记,即使输出相同。"DEBUG: "okok

如果事实证明不是我们所期望的那样,我们会去查看它是否正常工作。否则,我们可能需要在返回之前添加打印语句以检查:resultsome_functionother_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
  • 使用循环末尾的语句查看每次迭代后计数器变量的状态:printwhile

    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)

现在,每当我们想做一些调试时,我们可以设置(一个称为“标志”的)全局变量 ,当我们不想看到任何调试输入,我们可以将其转换为debugTrueFalse

交互式调试

许多程序员喜欢研究他们的代码的一种方法是使用交互式 REPL。 也就是说,您可以在终端直接运行函数并检查其输出。

通常,要完成此操作,您可以运行

python -i file.py

然后有一个 Python 会话,其中的所有定义都已经执行。file.py

如果您使用的是自动评分器,它有一个特定的工具,可让您跳到中间 失败的测试用例。只需运行ok

python ok -q <question name> -i

如果你有一个失败的测试用例,设置代码和doctest将打印在屏幕并继续运行,然后您将可以访问一个终端并在其中运行程序。

PythonTutor Debugging

有时是了解给定情况的最佳方式 一段 Python 代码是创建一个环境图。创建时手动绘制环境图有时可能很乏味,PythonTutor工具会自动创建环境图。要使用此工具,您可以复制粘贴代码到工具框中,并按运行。

或者,如果您正在使用自动计分,你可以运行ok

python ok -q <question name> --trace

并且应该会打开一个包含您的代码的浏览器窗口。【上面这个ok可以不管,是cs61a的课程计分工具】

使用语句assert

Python 有一个称为语句的功能,它允许您测试条件是否为真,如果否,则在一行中打印错误。如果您知道某些条件在某些点需要为真,这将非常有用。

在您的程序中。例如,如果您正在编写一个接受整数并将其加倍的函数,则可以确保您的输入实际上是整数。然后,您可以编写以下代码assert

def double(x):
    assert isinstance(x, int), "The input to double(x) must be an integer"
    return 2 * x

请注意,我们在这里并没有真正调试函数,我们所做的是确保任何调用的人都使用正确的参数来调试函数。例如,如果我们有一个接受字符串和数字的函数 并将字符串的长度添加到数字的两倍,它是这样实现的:doubledoubleg

def g(x, y):
    return double(x) + y # should be double(y) + len(x)

我们没有得到一个关于无法添加字符串和数字的奇怪错误,而是得到一个干净的错误,即参数必须是整数。这使我们能够快速缩小问题范围。double

assert 语句的一个主要好处是它们不仅仅是一个调试工具,你可以将它们永久地保留在代码中。软件开发的一个关键原则是,代码崩溃通常比产生不正确的结果更好,因此在代码中使用assert会使代码在有错误时,更有可能只是崩溃。

错误类型

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

SyntaxError

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

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

IndentationError

  • 原因:缩进不正确【因为python对缩进非常敏感】
  • 示例:

      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 将从函数的local frame开始。如果在那里找不到变量, Python 将继续搜索parent frame,直到它到达global frame。如果仍然找不到变量,Python就会报告错误。

IndexError

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

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

常见错误

拼写

Python 区分大小写。这通常会显示为 a ,但有时拼写错误的变量实际上已被定义。在这种情况下,它可以是很难发现它只是一个拼写错误。helloHellohelloheloNameError

缺少括号

一个常见的错误是省略右括号。这将显示为 .请考虑以下代码: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:

正确的应该是 

if x == 3:

无限循环

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

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

有时您可能增加了错误的计数器:

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

逐个错误

有时循环或递归函数也可能在短短一次迭代后就停止。出现这种情况时,最好是遍历迭代/递归,看看循环停止位置的编号是什么。while

掌握学习方法——作为开发者最重要的能力

掌握学习方法——作为开发者最重要的能力 (freecodecamp.org)

开始做这些事:

  1. 首先我需要确定一下我到底遇到了什么 Bug,之后假设几种导致它的可能性,即使我还不太了解,猜猜也罢。
  2. 接下来离开电脑跟前,稍微放松一下换换心情换换气。可其实心头上总是难以放下那个我没有解决了的问题。
  3. 接下来我根据我的假设开始调试——而不是直接去网上搜答案。要知道,全凭自己的思考获得答案是一种非常难能可贵的体验。即使你犯了很多错误,它也能加深你的印象好下次不再犯。
  4. 如果我的假设被验证了,那么万事大吉!反之,我就会去搜文档,博客,或者 Stack Overflow 或者任何其他可以辅助我的资料。
  5. 当我阅读这些资料的时候,我会把那些可能帮到忙的信息记录下来。
  6. 还是没有办法?没关系,即使我搜的这一大堆资料对解决目前的问题没有帮助,我也通过阅读学到了很多,而且它们也有可能解决我之后会遇到的问题。
  7. 直到这时,我才会在知乎上提个问题(译者注:原文为 Stack Overflow)或者问问同学同事。
  8. 与此同时,我会不断地重复研究这个问题,它总有被解决的时候。

有时候这个过程可能只需要几秒,而有时候需要几个小时甚至更久。无论怎样,在这个过程中你都能学到很多东西。

在编程中解决 Bug 就好像在隧道里寻找射灯一样。你总会找到它,可是沿途你会发现和了解更多有关隧道的信息。在学习上,这些了解正是你的收获。

调试错误其实是一个了解和发现的过程,享受它吧。

How to think like a programmer — lessons in problem solving 

像编程人员那样思考——解决问题的教训

为什么重要?
因为解决问题的能力是一个人的元技能.

我们都会遇到大大小小的问题,但有时候我们处理问题的方法却是随机的。

除非您有一个系统,否则可能您 "解决" 问题的方法就会象我开始编码时这样:

尝试一个解决方案,
如果不奏效,再尝试另一个方案,
如果仍不奏效,重复第 2 步,直到解决。
看,有时候您运气好,通过这种方法是把问题解决了,但这是解决问题最糟糕的方法!是对时间巨大的浪费。

解决问题最好的方法应是:

有一个框架
实践它
「几乎所有的雇主都把解决问题的能力放在首位。他们认为员工解决问题的能力是最重要的任职资格,而不仅仅是编程语言的熟练程度、调试能力和系统设计。展示计算思维或分解大型复杂问题的能力与工作所需的基本技术技能一样有价值 ,甚至可以说是更多。」 — Hacker Rank (《2018 开发者技能报告》)

“我认为刚入门的程序员犯的最大错误是:他们关注学习语法而不是学习如何解决问题。” 

当你遇到一个新问题时该怎么做了?

以下是步骤:

1. 理解
准确的理解问题。大部分艰难的问题之所以艰难是因为你不理解他们(因此,理解成为了第一步)。

那么你如何判断你已经明白了问题了?答案是当你能用自己的话去说明它的时候。

你是否想起遇到过的这种场景:你被一个难题困住了,然后你开始向别人说明这个问题,然后你很快发现之前没注意到的逻辑漏洞?

多数的程序员都了解这种感受。

所以,当你遇到问题时,你应该记录你的问题,针对问题画个草图,或者向别人描述这个问题。

“如果你无法使用简单词汇去向他人描述一件事情的时候,那么说明你还没理解它。” — Richard Feynman

2. 计划
不要在没有计划的情况下一头扎进去解决问题,虽然可能会莫名其妙地解决它,但是还是请规划好你的解决方案。

如果你没法记录正确的的步骤,那么一切都将是毫无帮助。

对于编程来说,这意味着你不要马上开始工作。你需要时间去分析这个问题和处理相关信息。

为了得到一个优秀的计划,请回答下面的问题:

“对于给出的输入 X ,需要经历哪些必要步骤才能得到输出 Y 了?”

注意:程序员们拥有一个优秀的工具来帮助他们完成这个,那就是注释!

3. 分治
请注意,这是最重要的一步。

不要去尝试直接解决一个复杂庞大的问题,这会让你疲惫不堪。

比较好的做法是,将一个大问题划分成若干个子问题,这些子问题比大问题更容易解决。

然后,一个接一个的去解决这些子问题,从最简单的子问题的开始。最简单的子问题就是那些你能直接得出答案或者更容易得出答案的问题。

最简单的意思是即将被解决的子问题不会依赖于其他子问题的解决情况。

一旦你解决了所有子问题,将他们串连起来。

串连起所有解决的子问题,这意味着你将得出起始问题的解决方案了。祝贺你!

这一步是解决问题的基础,请你牢记。(如果有必要,可以再读一遍这一步)。

“如果我可以教给每个初学者一个解决问题的技能,那么这一简化问题的方法成为我的不二选择。”

例如,假设你是个新手,你被要求写个程序,从输入 10 个数字中找出第三大的数字。对于一个完全新手来说,这将会是一个艰巨的任务,尽管这个问题只需要一些基本语法基础。

如果你卡住了,那么你应该想着把问题简化。比如我们的任务是找出第三大的数字,那么我们是不是先可以找出最大的数字了或者比较两个数字大小?

“你需要把问题简化到你可以解决的程度,并写出相应的解决方案。然后稍微拓展一下问题并且重新对问题写个解决方案。不断重复前面的操作直到你回到起始问题。

4. 卡住?
到现在为止,您可能坐在那里思考:“嘿,理查德…… 这很酷,但是如果我被困住甚至不能解决子问题怎么办?”

首先,深呼吸。其次,这很公平。

朋友,请不要担心。这发生在每个人身上!

不同之处在于,最好的程序员 / 问题解决者对错误 / 错误的好奇大于对错误的好奇。

实际上,面对混乱时,可以尝试以下三件事:

- 调试:逐步解决您的解决方案,以查找错误的地方。程序员称其为 debugging (实际上,这就是调试器所做的全部)。

“调试的技巧是弄清您真正告诉程序要执行的操作,而不是您认为要执行的操作。” — 安德鲁・辛格(Andrew Singer)

- 重新评估:退后一步。从另一个角度看问题。有什么可以抽象为更通用的方法的吗?

“有时我们对问题的细节迷失了,以至于忽略了一般性原则,而这些一般性原则将在更广泛的层面上解决问题。 […]

当然,经典的例子是一长串连续整数的总和,即 1 + 2 + 3 +…+ n,一个非常年轻的高斯很快就认出了 n(n + 1)/ 2,因此避免了必须做加法的工作。” — [C. 乔丹・鲍尔(Jordan Ball)(https://www.linkedin.com/in/cjordanball/)

旁注:重新评估的另一种方法是重新开始。删除所有内容,然后重新开始。我是认真的。您会惊讶于它的有效性。

- 研究:啊,Google 不错。您没看错。无论您遇到什么问题,都可能有人解决了。找到那个人 / 解决方案。实际上,即使您解决了问题,也要这样做! (您可以从其他人的解决方案中学到很多东西)。

警告:不要为大问题寻找解决方案。只寻找子问题的解决方案。为什么?因为除非您奋斗(一点点),否则您将不会学到任何东西。如果您什么都不学,那是在浪费时间。

实践
不要指望一周就可以变得很棒。如果你想成为一个好的问题解决者,请解决大量的问题!

实践实践再实践。意识到「使用「这里插入概念」可以很轻松的解决这个问题。」只是时间问题。

怎么实践?有大量的选择!

国际象棋、数学问题、数独、围棋、大富翁、电子游戏、区块猫等等。

实际上,在成功人士中,常见的习惯是练习 “解决微观问题” 的习惯。

这是否意味着您应该只玩电子游戏? 一点也不。

但是电子游戏到底是什么呢? 是的,解决问题!

因此,您应该做的是找到一个练习的途径。 使您能够解决大量微观问题的事物(最好是您喜欢的事物)。

例如,我喜欢编码方面的挑战。 每天,我都会尝试解决至少一个难题(通常在 Coderbyte)。

就像我说的,所有问题都有相似的模式。

「只要你认为自己已经成功克服了一个障碍,就会出现另一个障碍。 但这就是让生活变得有趣的原因。

生活是突破这些障碍的过程,这是我们必须突破的一系列防线。

每次你都会学到一些东西。

每次,你都能增加力量,智慧和远见。

每次,都会有更多的惊蛰者消失。 剩下的只有你:你最好的版本。」—— 瑞安・霍里(Ryan Holiday)

————————————————
原文作者:Summer
转自链接:https://learnku.com/cs/t/32774
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。

posted @ 2023-01-29 10:56  asandstar  阅读(217)  评论(0编辑  收藏  举报