├── dicegame ├── __init__.py ├── utils.py ├── die.py └── runner.py ├── solutions ├── dicegame │ ├── __init__.py │ ├── utils.py │ ├── die.py │ └── runner.py └── main.py ├── main.py ├── .gitignore ├── instructions.txt ├── LICENSE.txt └── README.md /dicegame/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /solutions/dicegame/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dicegame.runner import GameRunner 2 | 3 | 4 | def main(): 5 | print("Add the values of the dice") 6 | print("It's really that easy") 7 | print("What are you doing with your life.") 8 | GameRunner.run() 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /solutions/main.py: -------------------------------------------------------------------------------- 1 | from dicegame.runner import GameRunner 2 | 3 | 4 | def main(): 5 | print("Add the values of the dice") 6 | print("It's really that easy") 7 | print("What are you doing with your life.") 8 | GameRunner.run() 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /dicegame/utils.py: -------------------------------------------------------------------------------- 1 | class UnnecessaryError(Exception): 2 | pass 3 | 4 | 5 | def i_just_throw_an_exception(): 6 | value = 1 7 | def some_inner_function(): 8 | value += 1 9 | 10 | some_value = "I don't know what you were expecting" 11 | raise UnnecessaryError("You actually called this function...") 12 | -------------------------------------------------------------------------------- /solutions/dicegame/utils.py: -------------------------------------------------------------------------------- 1 | class UnnecessaryError(Exception): 2 | pass 3 | 4 | 5 | 6 | 7 | def i_just_throw_an_exception(): 8 | value = 1 9 | def some_inner_function(): 10 | value += 1 11 | 12 | some_value = "I don't know what you were expecting" 13 | raise UnnecessaryError("You actually called this function...") 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Extra 28 | *.sw[po] 29 | -------------------------------------------------------------------------------- /instructions.txt: -------------------------------------------------------------------------------- 1 | Your boss has given you the following project to fix for a client. It's supposed to be a simple dice 2 | game where the object of the game is to correctly add up the values of the dice for 6 consecutive turns. 3 | 4 | The issue is that a former programmer worked on it and didn't know how to debug effectively. It's now up to 5 | you to fix the errors and finally make the game playable. 6 | 7 | To play the game you must run the main.py file. 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Spiro Sideris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /solutions/dicegame/die.py: -------------------------------------------------------------------------------- 1 | # Just count the stupid dice 2 | import random 3 | 4 | def roll(dice): 5 | for die in dice: 6 | die.roll() 7 | 8 | class Die: 9 | """ 10 | This is always correct. Seriously, look away. 11 | """ 12 | 13 | def __init__(self): 14 | self.roll() 15 | 16 | def roll(self): 17 | self.value = int(random.random() * 6 + 1) 18 | 19 | def show(self): 20 | if self.value == 1: 21 | return("---------\n| |\n| * |\n| |\n---------") 22 | elif self.value == 2: 23 | return("---------\n|* |\n| |\n| *|\n---------") 24 | elif self.value == 3: 25 | return("---------\n|* |\n| * |\n| *|\n---------") 26 | elif self.value == 4: 27 | return("---------\n|* *|\n| |\n|* *|\n---------") 28 | elif self.value == 5: 29 | return("---------\n|* *|\n| * |\n|* *|\n---------") 30 | else: 31 | return("---------\n|* *|\n|* *|\n|* *|\n---------") 32 | 33 | @classmethod 34 | def create_dice(cls, n): 35 | return [cls() for _ in range(n)] 36 | -------------------------------------------------------------------------------- /dicegame/die.py: -------------------------------------------------------------------------------- 1 | # Just count the stupid dice 2 | import random 3 | 4 | def roll(dice): 5 | for die in dice: 6 | # XXX: I don't even know what this function does 7 | continue 8 | 9 | class Die: 10 | """ 11 | This is always correct. Seriously, look away. 12 | """ 13 | 14 | def __init__(self): 15 | self.roll() 16 | 17 | def roll(self): 18 | self.value = int(random.random() * 6 + 1) 19 | 20 | def show(self): 21 | if self.value == 1: 22 | return("---------\n| |\n| * |\n| |\n---------") 23 | elif self.value == 2: 24 | return("---------\n|* |\n| |\n| *|\n---------") 25 | elif self.value == 3: 26 | return("---------\n|* |\n| * |\n| *|\n---------") 27 | elif self.value == 4: 28 | return("---------\n|* *|\n| |\n|* *|\n---------") 29 | elif self.value == 5: 30 | return("---------\n|* *|\n| * |\n|* *|\n---------") 31 | else: 32 | return("---------\n|* *|\n|* *|\n|* *|\n---------") 33 | 34 | @classmethod 35 | def create_dice(cls, n): 36 | return [cls() for _ in range(n)] 37 | -------------------------------------------------------------------------------- /solutions/dicegame/runner.py: -------------------------------------------------------------------------------- 1 | from .die import Die 2 | from .utils import i_just_throw_an_exception 3 | 4 | class GameRunner: 5 | 6 | def __init__(self): 7 | self.dice = Die.create_dice(5) 8 | self.reset() 9 | 10 | def reset(self): 11 | self.round = 1 12 | self.wins = 0 13 | self.loses = 0 14 | 15 | @property 16 | def answer(self): 17 | total = 0 18 | for die in self.dice: 19 | total += die.value 20 | return total 21 | 22 | @classmethod 23 | def run(cls): 24 | count = 0 25 | runner = cls() 26 | while True: 27 | 28 | print("Round {}\n".format(runner.round)) 29 | 30 | for die in runner.dice: 31 | print(die.show()) 32 | 33 | guess = input("Sigh. What is your guess?: ") 34 | guess = int(guess) 35 | 36 | if guess == runner.answer: 37 | print("Congrats, you can add like a 5 year old...") 38 | runner.wins += 1 39 | count += 1 40 | runner.consecutive_wins += 1 41 | else: 42 | print("Sorry that's wrong") 43 | print("The answer is: {}".format(runner.answer)) 44 | print("Like seriously, how could you mess that up") 45 | runner.loses += 1 46 | count = 0 47 | print("Wins: {} Loses {}".format(runner.wins, runner.loses)) 48 | runner.round += 1 49 | 50 | if count == 6: 51 | print("You won... Congrats...") 52 | print("The fact it took you so long is pretty sad") 53 | break 54 | 55 | prompt = input("Would you like to play again?[Y/n]: ") 56 | 57 | if prompt.lower() == 'y' or prompt == '': 58 | continue 59 | else: 60 | break 61 | -------------------------------------------------------------------------------- /dicegame/runner.py: -------------------------------------------------------------------------------- 1 | from .die import Die 2 | from .utils import i_just_throw_an_exception 3 | 4 | class GameRunner: 5 | 6 | def __init__(self): 7 | self.dice = Die.create_dice(5) 8 | self.reset() 9 | 10 | def reset(self): 11 | self.round = 1 12 | self.wins = 0 13 | self.loses = 0 14 | 15 | def answer(self): 16 | total = 0 17 | for die in self.dice: 18 | total += 1 19 | return total 20 | 21 | @classmethod 22 | def run(cls): 23 | # Probably counts wins or something. 24 | # Great variable name, 10/10. 25 | c = 0 26 | while True: 27 | runner = cls() 28 | 29 | print("Round {}\n".format(runner.round)) 30 | 31 | for die in runner.dice: 32 | print(die.show()) 33 | 34 | guess = input("Sigh. What is your guess?: ") 35 | guess = int(guess) 36 | 37 | if guess == runner.answer(): 38 | print("Congrats, you can add like a 5 year old...") 39 | runner.wins += 1 40 | c += 1 41 | else: 42 | print("Sorry that's wrong") 43 | print("The answer is: {}".format(runner.answer())) 44 | print("Like seriously, how could you mess that up") 45 | runner.loses += 1 46 | c = 0 47 | print("Wins: {} Loses {}".format(runner.wins, runner.loses)) 48 | runner.round += 1 49 | 50 | if c == 6: 51 | print("You won... Congrats...") 52 | print("The fact it took you so long is pretty sad") 53 | break 54 | 55 | prompt = input("Would you like to play again?[Y/n]: ") 56 | 57 | if prompt == 'y' or prompt == '': 58 | continue 59 | else: 60 | i_just_throw_an_exception() 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pdb` 教程 2 | 3 | 这篇教程的目的是教你基本的 `pdb` 用法,它是 `Python2` 和 `Python3` 的调试器。这篇教程也会涉及到一些有用的小技巧,让你在调试程序的时候轻松很多。 4 | 5 | --- 6 | 7 | 这篇教程最好搭配 Python 2.7 或者 Python 3.4 使用,如果 `pdb` 在这两个 Python 版本上表现有所不同,我会突出显示他们的差异。要检查你现在使用的 Python 版本,你可以在你的终端上运行如下的命令: 8 | 9 | ```shell 10 | python --version 11 | ``` 12 | 13 | 现在你知道你的 Python 版本了,让我们开始吧! 14 | 15 | ## 调试器的功能是什么? 16 | 17 | 在真正开始学习调试代码之前,我们应该先简要地谈论一下调试代码和使用调试工具的重要性。对我来说,如下的三点展现了调试器的重要性。 18 | 19 | 通过一个调试器,你可以做到 20 | 21 | * 检查一个正在运行的程序的状态 22 | * 在程序投入使用前测试它的代码 23 | * 追踪程序的执行逻辑 24 | 25 | 通过使用一个调试器,你可以在你的程序的任意一个地方设置[程序断点](https://en.wikipedia.org/wiki/Breakpoint)来停止运行程序,也可以做到上面提到的三点。调试器是很强大的工具,和简单地使用 `print()` 语句相比,它们可以大大加快调试过程。 26 | 27 | 28 | 29 | 对于你们当中经验丰富的程序员来说,你们也许会同意我的观点:在最好的程序员和懂得如何有效调试程序的程序员之间是存在相关性的。这里所说的有效调试程序指的是:能够诊断出代码的问题,然后可以用最小的力气解决它。使用一个调试器和学习如何正确使用一个调试器会帮助你成为强大的调试高手。为了能够做到在调试环境中轻松自如你得花上不少时间,不过本教程的目的就是让你在自己的代码里面使用 `pdb` 之前先经历这些! 30 | 31 | ## 玩游戏 32 | 33 | 我们已经讨论了调试器的功能,现在到了亲自看看它是如何工作的时候了。首先,如果你还没有克隆这个仓库的话要先克隆。如果你还没有安装 `git` 的话,我推荐你尝试安装并使用它(或者是其他的源代码工具),你可以参考[这里](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)来安装 `git`。在你安装好了 `git` 之后,在你的终端运行如下的命令来克隆仓库: 34 | 35 | ```shell 36 | git clone https://github.com/spiside/pdb-tutorial 37 | ``` 38 | 39 | **注意**:如果这个命令报错克隆失败的话,你应该看看 Github 的[克隆教程](https://help.github.com/articles/cloning-a-repository/) 40 | 41 | 42 | 43 | 现在你克隆好了仓库,让我们来到项目的根目录并看看里面的说明文件: 44 | 45 | ```shell 46 | cd /path/to/pdb-tutorial 47 | ``` 48 | 49 | `file: instructions.txt` 50 | 51 | ``` 52 | Your boss has given you the following project to fix for a client. It's supposed to be a simple dice 53 | game where the object of the game is to correctly add up the values of the dice for 6 consecutive turns. 54 | 55 | The issue is that a former programmer worked on it and didn't know how to debug effectively. 56 | It's now up to you to fix the errors and finally make the game playable. 57 | 58 | To play the game you must run the main.py file. 59 | ``` 60 | 61 | 这看起来很容易!首先,让我们尝试玩这个游戏并看看哪里出了问题。你可以在你的终端输入如下的命令来运行这个程序: 62 | 63 | ```shell 64 | python main.py 65 | ``` 66 | 67 | 你应该会看到像这样的输出: 68 | 69 | ``` 70 | Add the values of the dice 71 | It's really that easy 72 | What are you doing with your life. 73 | Round 1 74 | 75 | --------- 76 | |* | 77 | | | 78 | | *| 79 | --------- 80 | --------- 81 | |* | 82 | | | 83 | | *| 84 | --------- 85 | --------- 86 | |* *| 87 | | | 88 | |* *| 89 | --------- 90 | --------- 91 | |* *| 92 | | | 93 | |* *| 94 | --------- 95 | --------- 96 | |* *| 97 | | * | 98 | |* *| 99 | --------- 100 | Sigh. What is your guess?: 101 | ``` 102 | 103 | 看起来之前的程序员似乎有点...幽默感?尽管如此,让我们来输入 17(因为 17 是骰子的和)。 104 | 105 | ``` 106 | Sigh. What is your guess?: 17 107 | Sorry that's wrong 108 | The answer is: 5 109 | Like seriously, how could you mess that up 110 | Wins: 0 Loses 1 111 | Would you like to play again?[Y/n]: 112 | ``` 113 | 114 | 奇怪。他说答案应该是 5,但显然不对...。好吧,也许骰子的加法是错误的,无论如何我们再玩一次游戏来弄清楚。看起来要再玩一次的命令是 `Y`,所以让我们输入 `Y` 115 | 116 | ``` 117 | Would you like to play again?[Y/n]: Y 118 | Traceback (most recent call last): 119 | File "main.py", line 12, in 120 | main() 121 | File "main.py", line 8, in main 122 | GameRunner.run() 123 | File "/Users/Development/pdb-tutorial/dicegame/runner.py", line 62, in run 124 | i_just_throw_an_exception() 125 | File "/Users/Development/pdb-tutorial/dicegame/utils.py", line 13, in i_just_throw_an_exception 126 | raise UnnecessaryError("You actually called this function...") 127 | dicegame.utils.UnnecessaryError: You actually called this function... 128 | ``` 129 | 130 | 好吧,这很奇怪,即使我们输入了应该是有效的输入,程序仍然抛出了异常。我认为我们可以下结论说这个程序是有问题的,那么让我们开始调试程序吧! 131 | 132 | ## PDB 101: `pdb` 介绍 133 | 134 | 是时候到了使用 python 自带的调试器 `pdb` 的时候了。这个调试器包含在 python 的标准库中,我们就像使用任何一个 python 库一样来使用它。首先,我们应先加载 `pdb` 模块,然后调用它的方法在程序中设置用于调试的程序断点。传统的方法是把加载和调用放在你想要程序暂停运行的地方。这个是你会用到的完整声明语句: 135 | 136 | ```python 137 | import pdb; pdb.set_trace() 138 | ``` 139 | 140 | [`set_trace()`](https://docs.python.org/3/library/pdb.html#pdb.set_trace) 方法在被调用的地方设置了一个程序断点。让我们来打开 `main.py` 文件并在第 8 行添加程序断点: 141 | 142 | 143 | 144 | `file: main.py` 145 | 146 | ```python 147 | from dicegame.runner import GameRunner 148 | 149 | 150 | def main(): 151 | print("Add the values of the dice") 152 | print("It's really that easy") 153 | print("What are you doing with your life.") 154 | import pdb; pdb.set_trace() # add pdb here 155 | GameRunner.run() 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | ``` 161 | 162 | 看起来不错,现在让我们再次尝试运行 `main.py` 文件并看看会怎么样。 163 | 164 | ```shell 165 | python main.py 166 | ``` 167 | 168 | ``` 169 | Add the values of the dice 170 | It's really that easy 171 | What are you doing with your life. 172 | > /Users/Development/pdb-tutorial/main.py(9)main() 173 | -> GameRunner.run() 174 | (Pdb) 175 | ``` 176 | 177 | 很好,我们现在处于运行程序的中间,可以开始调试程序了。我认为第一个要解决的问题是骰子之间的加法到底是如何定义的。 178 | 179 | 如果你很熟悉 Python 的解释器,里面有很多知识可以迁移到 `pdb` 调试器中来。但是,当我们学习到高阶部分的时候将会有一些问题。先不管这些,我们来学习一些会帮助我们解决骰子加法问题的命令。 180 | 181 | ## 5 个会让你“说不出话来”的 `pdb` 命令 182 | 183 | 下面是直接从 `pdb` 文档中摘录出来的命令,一旦你学会了使用他们,你就无法想象没有他们会是怎么样。 184 | 185 | 1. `l(ist)` - 展示当前行附近的 11 行,或者是继续之前的展示。 186 | 2. `s(tep)` - 执行当前行,会停在第一个可能停下的地方。 187 | 3. `n(ext)` - 继续执行程序到当前函数的下一行或者是到它返回结果。 188 | 4. `b(reak)` - 设置程序断点 (取决于提供的参数)。 189 | 5. `r(eturn)` - 继续执行到当前函数返回结果。 190 | 191 | 注意这些命令中的后面都用括号括了起来,这表示在 `pdb` 中使用这些命令的时候,括号中的部分是可选的,也就是说是可以省略的。这会节省你打字的时间,但是如果你有变量名是 `l` 或者是 `n` 的话会有大问题,他们会被认为是 `pdb` 的命令而不是变量。举例来说,如果在你的程序中有变量叫做 `c`,你想知道它的值,你如果直接在 `pdb` 里输入 `c` 的话,实际上你是在告诉调试器说要执行 `c(ontinue)` 指令,它会一直运行直到遇到程序断点! 192 | 193 | 194 | 195 | **注意**:我和其他程序员们一样,不建议使用短的变量名,比如 `a`,`b`,`gme`等。这些短变量名没有含义还会让其他人在阅读你的代码的时候感到很困惑。我只是在这里向你们展示你们在 `pdb` 中使用短变量名的时候可能会遇到的问题。 196 | 197 | 198 | 199 | **再次注意**:另外一个有用的命令是 `h(elp) - 不给参数的话,会输出可使用的所有命令。如果提供了命令作为参数,就会输出命令的帮助手册`。这篇教程接下来的部分,我会使用短命令,如果我用到了一个我没有介绍过的命令,我会解释它的功能是什么。那么,让我们开始学习第一个命令吧。 200 | 201 | ### 1. `l(ist)` 又叫做: 我懒得打开包含源代码的文件 202 | 203 | ``` 204 | l(ist) [first [,last]] 205 | List source code for the current file. Without arguments, list 11 lines around the current line 206 | or continue the previous listing. With one argument, list 11 lines starting at that line. 207 | With two arguments, list the given range; if the second argument is less than the first, it is a count. 208 | ``` 209 | 210 | **注意**:上面的命令描述是调用 `help list` 生成的。如果要得到一样的输出,你可以在 `pdb` 中输入 `help l`。我们可以使用 `list` 命令来检查我们所在的行的源代码。`list` 命令的参数让你可以指定要查看的行的范围,这在你用来查看第三方库的时候很有用(往往代码会很长)。 211 | 212 | 213 | 214 | **注意**:在大于等于 Python 3.2 的版本中,你可以通过输入 `ll`(更长的输出)来看当前函数或者是堆栈帧的源代码。我基本都是使用这个而不是 `l` 命令,因为它相比于展示当前位置附近的 11 行来说,你可以更直观知道你当前处在哪个函数里。 215 | 216 | 217 | 218 | 现在让我们尝试使用 `l` 命令。在你已经打开的 `pdb` 调试窗口中,输入 `l` 并查看输出: 219 | 220 | ```python 221 | (Pdb) l 222 | 4 def main(): 223 | 5 print("Add the values of the dice") 224 | 6 print("It's really that easy") 225 | 7 print("What are you doing with your life.") 226 | 8 import pdb; pdb.set_trace() 227 | 9 -> GameRunner.run() 228 | 10 229 | 11 230 | 12 if __name__ == "__main__": 231 | 13 main() 232 | [EOF] 233 | ``` 234 | 235 | 如果我们想看整个文件,我们可以给 list 命令加上范围参数(1~13),像这样: 236 | 237 | ```python 238 | (Pdb) l 1, 13 239 | 1 from dicegame.runner import GameRunner 240 | 2 241 | 3 242 | 4 def main(): 243 | 5 print("Add the values of the dice") 244 | 6 print("It's really that easy") 245 | 7 print("What are you doing with your life.") 246 | 8 import pdb; pdb.set_trace() 247 | 9 -> GameRunner.run() 248 | 10 249 | 11 250 | 12 if __name__ == "__main__": 251 | 13 main() 252 | ``` 253 | 254 | 不幸的是,我们从这个文件中得不到什么信息,但我们可以看到它调用了 `GameRunner` 类的 `run()` 方法。此时,你可能会想,“哇哦,我要在 `dicegame/runner.py` 文件里面的 run 方法中插入 `pdb` 语句!“这个方法是可行的,但是更轻松的方式是使用我们之后会提到的 `step` 命令。 255 | 256 | ### 2. `s(tep)` 又叫做: 让我们来看看这个方法做了什么... 257 | 258 | ``` 259 | s(tep) 260 | Execute the current line, stop at the first possible occasion 261 | (either in a function that is called or in the current 262 | function). 263 | ``` 264 | 265 | 你的程序现在应该还停留在 `:9` 行,要知道当前在哪一行可以查看 `list` 命令输出的 `->` 箭头指向哪。 266 | 267 | 268 | 269 | 让我们来调用 `step` 命令看看会发生什么。 270 | 271 | ```python 272 | (Pdb) s 273 | --Call-- 274 | > /Users/Development/pdb-tutorial/dicegame/runner.py(21)run() 275 | -> @classmethod 276 | ``` 277 | 278 | 不错!我们目前在 `runner.py` 文件的第 21 行,这一点可以从 `> /Users/Development/pdb-tutorial/dicegame/runner.py(21)run()` 看出来。 279 | 280 | 问题是,我们没有太多的上下文信息,所以让我们来运行 `list` 命令来检查这个方法。 281 | 282 | ```python 283 | (Pdb) l 284 | 16 total = 0 285 | 17 for die in self.dice: 286 | 18 total += 1 287 | 19 return total 288 | 20 289 | 21 -> @classmethod 290 | 22 def run(cls): 291 | 23 # Probably counts wins or something. 292 | 24 # Great variable name, 10/10. 293 | 25 c = 0 294 | 26 while True: 295 | ``` 296 | 297 | 哇哦!现在我们有了 `run()` 方法的更多上下文信息。但我们现在还在 `:21` 行。让我们再次执行 `step` 命令来进入到这个方法内部,然后用 `list` 命令查看我们当前的位置。 298 | 299 | ```python 300 | (Pdb) s 301 | > /Users/Development/pdb-tutorial/dicegame/runner.py(25)run() 302 | -> c = 0 303 | (Pdb) l 304 | 20 305 | 21 @classmethod 306 | 22 def run(cls): 307 | 23 # Probably counts wins or something. 308 | 24 # Great variable name, 10/10. 309 | 25 -> c = 0 310 | 26 while True: 311 | 27 runner = cls() 312 | 28 313 | 29 print("Round {}\n".format(runner.round)) 314 | 30 315 | ``` 316 | 317 | 正如我们所看到的,我们正处于一个糟糕的变量名 `c` 上,如果我们尝试调用它将会产生很大的问题(回忆之前我们关于 `c(ontinue)` 命令的讨论)。我们正处于 `while` 循环前,所以让我们来进入到循环里面看看我们能发现什么。 318 | 319 | ### 3. `n(ext)` 又叫做: 我希望当前的行不要抛出异常 320 | 321 | ``` 322 | n(ext) 323 | Continue execution until the next line in the current function 324 | is reached or it returns. 325 | ``` 326 | 327 | 在当前行输入 `n(ext)` 命令,然后输入 `list`(注意这个模式),然后让我们看看发生了什么。 328 | 329 | 330 | 331 | **译者注**:在我用的 Python 3.8.10 上,此时我停留在了 `runner = cls()` 这一行 332 | 333 | ```python 334 | (Pdb) n 335 | > /Users/Development/pdb-tutorial/dicegame/runner.py(27)run() 336 | -> while True: 337 | (Pdb) l 338 | 21 @classmethod 339 | 22 def run(cls): 340 | 23 # Probably counts wins or something. 341 | 24 # Great variable name, 10/10. 342 | 25 c = 0 343 | 26 -> while True: 344 | 27 runner = cls() 345 | 28 346 | 29 print("Round {}\n".format(runner.round)) 347 | 30 348 | 31 for die in runner.dice: 349 | ``` 350 | 351 | 现在我们停在了 `while True` 语句!我们可以一直调用 `next` 命令直到程序抛出异常或者是结束运行。再调用 3 次 `next` 命令就来到了 `for` 语句,然后调用 `list` 命令展示当前行的附近行。 352 | 353 | 354 | 355 | **译者注**:因为之前直接跳到了 `runner = cls()` 这一行,所以在这里我只要调用 2 次 `next` 就到了 `for` 语句 356 | 357 | ```python 358 | (Pdb) n 359 | > /Users/Development/pdb-tutorial/dicegame/runner.py(27)run() 360 | -> runner = cls() 361 | (Pdb) n 362 | > /Users/Development/pdb-tutorial/dicegame/runner.py(29)run() 363 | -> print("Round {}\n".format(runner.round)) 364 | (Pdb) n 365 | Round 1 366 | 367 | > /Users/Development/pdb-tutorial/dicegame/runner.py(31)run() 368 | -> for die in runner.dice: 369 | (Pdb) l 370 | 26 while True: 371 | 27 runner = cls() 372 | 28 373 | 29 print("Round {}\n".format(runner.round)) 374 | 30 375 | 31 -> for die in runner.dice: 376 | 32 print(die.show()) 377 | 33 378 | 34 guess = input("Sigh. What is your guess?: ") 379 | 35 guess = int(guess) 380 | ``` 381 | 382 | 如果你继续输入 `next` 命令,你会遍历完这个 `for` 循环,遍历的长度等于 `runner.dice` 的长度。我们可以在 `pdb` 中用 `len` 方法来看看 `runner.dice` 的长度,应该会返回 5。 383 | 384 | ```python 385 | (Pdb) len(runner.dice) 386 | 5 387 | ``` 388 | 389 | 因为这个长度*只有* 5,我们可以调用 5 次 `next` 命令来走完这个循环,但如果长度是 50 甚至是 10000 呢! 390 | 391 | 更好的方法应该是设置一个程序断点,然后调用 `continue` 命令一直运行到程序断点处。 392 | 393 | ### 4. `b(reak)` 又叫做:我再也不想输入 `n` 命令了 394 | 395 | ``` 396 | b(reak) [ ([filename:]lineno | function) [, condition] ] 397 | Without argument, list all breaks. 398 | 399 | With a line number argument, set a break at this line in the 400 | current file. With a function name, set a break at the first 401 | executable line of that function. If a second argument is 402 | present, it is a string specifying an expression which must 403 | evaluate to true before the breakpoint is honored. 404 | 405 | The line number may be prefixed with a filename and a colon, 406 | to specify a breakpoint in another file (probably one that 407 | hasn't been loaded yet). The file is searched for on 408 | sys.path; the .py suffix may be omitted. 409 | ``` 410 | 411 | 在这篇教程中,我们只需要看 `b(reak)` 命令描述的前两段。就像我在前面的部分提到的,我们想要通过设置程序断点的方式来执行完 `for` 循环,然后接着看 `run()` 方法的其他部分。因为 `:34` 行有 `input` 函数,程序会暂停并等待用户输入,所以让我们停在 `:34` 行。为了做到这一点,我们可以输入 `b 34` 然后输入 `continue` 来运行到程序断点处。 412 | 413 | ```python 414 | (Pdb) b 34 415 | Breakpoint 1 at /Users/Development/pdb-tutorial/dicegame/runner.py(34)run() 416 | (Pdb) c 417 | 418 | [...] # prints some dice 419 | 420 | > /Users/Development/pdb-tutorial/dicegame/runner.py(34)run() 421 | -> guess = input("Sigh. What is your guess?: ") 422 | ``` 423 | 424 | 我们也可以通过不带参数调用 `break` 命令来看看我们设置好的程序断点。 425 | 426 | ```python 427 | (Pdb) b 428 | Num Type Disp Enb Where 429 | 1 breakpoint keep yes at /Users/Development/pdb-tutorial/dicegame/runner.py:34 430 | breakpoint already hit 1 time 431 | ``` 432 | 433 | 如果要删除你的程序断点,你可以使用 `cl(ear)` 命令,参数是上面输出的程序断点行的最左边(也就是 1)。现在让我们调用参数为 1 的 `clear` 命令来删除刚才设置的程序断点。 434 | 435 | 436 | 437 | **注意**:如果你没有提供任何参数给 `clear` 命令,那么就会清空所有的程序断点。 438 | 439 | ```python 440 | (Pdb) cl 1 441 | Deleted breakpoint 1 at /Users/Development/pdb-tutorial/dicegame/runner.py:34 442 | ``` 443 | 444 | 现在我们可以调用 `next` 命令来执行 `input()` 函数。让我们猜测骰子和为 10 并输入我们的猜测,一旦我们回到 `pdb` 中,我们可以调用 `list` 命令来看看接下来的几行。 445 | 446 | ```python 447 | (Pdb) n 448 | Sigh. What is your guess?: 10 449 | > /Users/Development/pdb-tutorial/dicegame/runner.py(35)run() 450 | -> guess = int(guess) 451 | (Pdb) l 452 | 30 453 | 31 for die in runner.dice: 454 | 32 print(die.show()) 455 | 33 456 | 34 guess = input("Sigh. What is your guess?: ") 457 | 35 -> guess = int(guess) 458 | 36 459 | 37 if guess == runner.answer(): 460 | 38 print("Congrats, you can add like a 5 year old...") 461 | 39 runner.wins += 1 462 | 40 c += 1 463 | ``` 464 | 465 | 记住我们是在尝试找出第一次玩游戏的时候我们猜测的骰子和不对的原因。这看起来似乎是 `guess == runner.answer` 这里的等于判断出了问题。我们应该仔细检查一下看看 `runner.answer()` 这个方法做了什么,以防万一可能会遇到错误。调用一次 `next` 命令然后调用 `step` 命令来*进入*到 `runner.answer()` 方法内部。 466 | 467 | ```python 468 | (Pdb) s 469 | --Call-- 470 | > /Users/spiro/Development/mobify/engineering-meeting/pdb-tutorial/dicegame/runner.py(15)answer() 471 | -> def answer(self): 472 | (Pdb) l 473 | 10 def reset(self): 474 | 11 self.round = 1 475 | 12 self.wins = 0 476 | 13 self.loses = 0 477 | 14 478 | 15 -> def answer(self): 479 | 16 total = 0 480 | 17 for die in self.dice: 481 | 18 total += 1 482 | 19 return total 483 | 20 484 | ``` 485 | 486 | 我想我找到了问题根源!在第 18 行,它在计算 `total` 的时候看起来不是我们想象的那种骰子的加法。让我们来检查一下 `die` 有没有什么属性等于骰子本身的值看能不能解决这个问题。如果要到第 18 行,你可以通过设置一个程序断点,也可以一直调用 `next` 命令直到你到了循环到第一次迭代。一旦你到了 `:18` 行,让我们对 `die` 实例调用一下 `dir()` 函数看看它有什么属性和方法。 487 | 488 | ```python 489 | -> total += 1 490 | (Pdb) dir(die) 491 | ['__class__', '__delattr__', [...], 'create_dice', 'roll', 'show', 'value'] 492 | ``` 493 | 494 | 它有一个属性叫做 `value`!让我们调用一下它看看返回了什么(注意,你这里显示的值可能跟我的不同)。同时让我们也调用一下它的 `show()` 方法显示骰子图案,确保显示的结果和它的 `value` 值一样。 495 | 496 | ```python 497 | (Pdb) die.value 498 | 2 499 | (Pdb) die.show() 500 | '---------\n|* |\n| |\n| *|\n---------' 501 | ``` 502 | 503 | **注意**:如果你想要换行符 `\n` 真的有换行的效果,你可以执行 `print(die.show())`。 504 | 505 | 506 | 507 | 这看起来一切正常,骰子的 `value` 值和 `show()` 方法输出的是一样的,我们准备修复 answer 方法。但是,我们中的一些人可能想要继续调试程序来一次性找出所有的程序错误。不幸的是,我们再一次卡在了 for 循环里,你可能想到了在第 `:19` 行设置一个程序断点然后调用 `continue` 命令,但实际上有更好的方法。 508 | 509 | ### 5. `r(eturn)` 又叫做:我想退出这个函数 510 | 511 | ``` 512 | r(eturn) 513 | Continue execution until the current function returns. 514 | ``` 515 | 516 | `return` 是一个*强大的用户命令*,让你可以直接检查函数的最后返回的结果。尽管你可以在调用 return 的地方设置一个程序断点,但如果一个函数里有多个 return 语句的话,还是在 pdb 里面使用 `return` 命令会好一些,因为它对一个 return 语句只会遵循一条执行路径。让我们调用一下 `return` 命令到函数的末尾。 517 | 518 | ```python 519 | (Pdb) r 520 | --Return-- 521 | > /Users/Development/pdb-tutorial/dicegame/runner.py(19)answer()->5 522 | -> return total 523 | (Pdb) l 524 | 14 525 | 15 def answer(self): 526 | 16 total = 0 527 | 17 for die in self.dice: 528 | 18 total += 1 529 | 19 -> return total 530 | 20 531 | 21 @classmethod 532 | 22 def run(cls): 533 | 23 # Probably counts wins or something. 534 | 24 # Great variable name, 10/10. 535 | (Pdb) 536 | ``` 537 | 538 | 如果要检查返回的 `total` 变量的值,你可以在这里输入 `total` 或者直接看 `--Return--` 下面的语句(最右边)。现在,为了回到 `run()` 方法里,让我们调用 `next` 命令,就会回到原来的地方。 539 | 540 | 541 | 542 | 此时,你可以通过调用 `exit()` 命令**或者**按下 `CTRL+D`(和在 Python REPL 环境中一样)。有了这 5 个命令,你应该能弄明白其他的一些 bug,然后可以进阶学习更高阶的 `pdb` 例子。 543 | 544 | ## 高阶 `pdb` 545 | 546 | 这里有一些你可以使用的高阶的 `pdb` 命令。 547 | 548 | ### `!` 命令 549 | 550 | ``` 551 | ! 552 | Execute the (one-line) statement in the context of the current stack frame. 553 | ``` 554 | 555 | `! `命令是在告诉 `pdb` 接下来的语句是 Python 命令而不是 `pdb` 命令。在带有变量名为 `c` 的 `run()` 方法中这很有用。就像我在教程一开始说的,直接在 `pdb` 里面输入 `c` 会被认为是要执行 `continue` 指令。进入 `pdb` 调试模式,停在 `runner.py` 的 `:26` 行,然后可以运行 `!c` 看看会发生什么。 556 | 557 | ```python 558 | (Pdb) !c 559 | 0 560 | ``` 561 | 562 | 我们得到了预料中的值,因为在 `:25` 行执行了 `c = 0`! 563 | 564 | ### `pdb` Post Mortem 565 | 566 | ``` 567 | pdb.post_mortem(traceback=None) 568 | Enter post-mortem debugging of the given traceback object. If no traceback is given, it uses the one of the exception that is currently being handled 569 | (an exception must be being handled if the default is to be used). 570 | 571 | pdb.pm() 572 | Enter post-mortem debugging of the traceback found in sys.last_traceback. 573 | ``` 574 | 575 | 尽管这两个方法可能看上去一样,但是 `post_mortem()` 和 `pm()` 产生的异常信息轨迹不一样。我通常在 `except` 的代码块里面使用 `post_mortem()`。 576 | 577 | 但是,我也会谈到 `pm()`,因为我发现它更强大。让我们来尝试这个方法并看看在实践中它是怎么工作的。 578 | 579 | 580 | 581 | 先打开终端并进入到项目的根目录下,然后输入 `python` 进 python REPL 交互模式。然后,让我们从 `main` 模块里面载入 `main` 方法,同时要载入 `pdb`。接下来玩游戏直到我们在尝试输入 `Y` 继续游戏的时候抛出异常。 582 | 583 | ```python 584 | >>> import pdb 585 | >>> from main import main 586 | >>> main() 587 | [...] 588 | Would you like to play again?[Y/n]: Y 589 | Traceback (most recent call last): 590 | File "main.py", line 12, in 591 | main() 592 | File "main.py", line 8, in main 593 | GameRunner.run() 594 | File "/Users/Development/pdb-tutorial/dicegame/runner.py", line 62, in run 595 | i_just_throw_an_exception() 596 | File "/Users/Development/pdb-tutorial/dicegame/utils.py", line 13, in i_just_throw_an_exception 597 | raise UnnecessaryError("You actually called this function...") 598 | dicegame.utils.UnnecessaryError: You actually called this function... 599 | ``` 600 | 601 | 现在,让我们从 `pdb` 模块中调用 `pm()` 方法看看会发生什么。 602 | 603 | ```python 604 | >>> pdb.pm() 605 | > /Users/Development/pdb-tutorial/dicegame/utils.py(13)i_just_throw_an_exception() 606 | -> raise UnnecessaryError("You actually called this function...") 607 | (Pdb) 608 | ``` 609 | 610 | 看看!在 `pdb` 环境中我们停在了最后一个抛出异常的地方的前面。在这里我们可以在程序出错之前检查程序的状态。 611 | 612 | 613 | 614 | **注意**:你也可以使用 `python -m pdb main.py` 来启动 `main.py` 脚本,然后输入 `continue` 运行程序直到抛出异常,Python 会在没有捕获的异常上自动进入 `post_mortem` 模式。 615 | 616 | ## 写在最后 617 | 618 | 恭喜你坚持到了最后,谢谢你一路学习了这篇教程!如果你有任何意见、批评或者额外的高阶例子想提供,欢迎你提交 pull request。 619 | 620 | --------------------------------------------------------------------------------