# 如何阅读代码

从根本上说，程序员与代码交流。我们不仅向计算机，也向其他开发人员表达了我们的想法。到目前为止，我们专注于设计程序和编写 Python 代码。这是关键的创作过程，但是，为了编写代码，程序员必须能够阅读其他人编写的代码。

## 为什么阅读代码

我们阅读代码以便：

* **获得新体验**。就像在自然语言中，我们通过倾听他人来学习说话一样，我们通过识别他人代码中的酷炫模式来学习编程技巧。能够快速阅读代码，使您可以获得观看编程讲座或视频的经验。
* **查找并修改代码段**。我们经常可以通过试用 Google 搜索或 [StackOverlow](https://stackoverflow.com/)找到的代码段，找到编码问题的提示或解决方案。 请注意，您不违反版权法，如果是学生项目，则不违反学术诚信规则。
* **发现库函数或其他共享代码的行为**。 从名称或参数列表中并不总是清楚库函数的完整行为。 查看该函数的源代码是了解它的作用的最佳方法。代码**就是**文档。
* **在我们的代码或其他代码中发现错误**。 所有代码都有错误，特别是我们刚刚编写，但没有经过详尽测试的代码。作为编码过程的一部分，我们不断跳来跳去，阅读我们现有的代码库，来确保一切都组合在一起。

<img src="img/redbang.png" width="30" align="left">

在我们讨论库函数时，让我强调一条黄金法则：*你永远不应该向你的程序员询问参数的细节和库函数的返回值。*你可以通过 PyCharm 中的“跳转到定义”或网络搜索来自己发现它。

本文档的目的是解释程序员如何读取代码。 我们的第一个线索来自于我们不是计算机这一事实，因此，我们不应该像计算机一样阅读代码，一个接一个地检查一个符号。 相反，我们将寻找关键元素和代码模式。

这就是我们用外语阅读句子时所做的事情。 例如，我的法语非常糟糕，因此，在阅读法语句子时，我必须有意识地询问*谁在对谁做什么*。在实践中，这意味着识别主语，动词和宾语。从这些关键要素中，我试图想象作者心中的思维模式。基本上我试图反转作者所遵循的过程。

在编程世界中，过程如下：代码作者可能会想到“*通过除以 2 *将价格转换为新列表”，然后将它们转换为“映射”的伪代码，最后转换为 Python `for`循环。在阅读循环代码时，我们的工作是反转过程，并想象作者的原始目标。 我们不是试图通过在我们的头脑或纸上模拟它，来弄清楚代码的突现行为；相反，我们正在寻找模式，它们能够告诉我们正在执行哪些高级操作。

这就是为什么在编写代码时应该强调清晰度，以便读者阅读更多内容。[约翰 F. 伍兹](https://groups.google.com/forum/#!msg/comp.lang.c++/rYCO5yn4lXw/oITtSkZOtoUJ) 有一个很好的引言，总结了很多东西：

> 写代码的时候总是想象，维护你代码的家伙是一个知道你住在哪里的暴力精神病患者。

## 获得程序的要点

在第一次查看教科书时，扫描目录来获得书籍内容的整体视图，是有意义的。 第一次看节目时也是如此。 查看所有文件以及这些文件中包含的函数的名称。 同时，找出主程序的位置。 根据您在程序中的目标，您可能会开始单步执行主程序或立即跳转到感兴趣的函数。

从样例运行或单元测试中查看程序的输入 - 输出对也很有用，因为它可以帮助您了解程序的功能。 从某种意义上说，我们通过检查和测试程序，对程序的工作计划进行逆向工程。 以前，我们在前进方向使用程序的工作计划来设计程序。

## 获得函数的要点

一旦我们确定了要检查的主程序或函数，就应该对函数的工作计划进行反向工程。 函数的名称可能是函数功能的最大线索，假设代码作者是一个不错的程序员。 （使用像`f`这样的通用函数名称，是教师在不泄露答案的情况下，编写代码阅读问题的方式。）例如，毫无疑问，以下函数的目标是什么：

```python
def average(...):
    ...
```

即使不查看参数或函数语句。

程序员通常会提供函数用法的注释，但要小心。 程序员通常会在不更改注释的情况下更改代码，因此注释会产生误导。可接受的注释可能如下所示：

```python
def average(...):
    "Compute and return the average of a list of numbers"
    ...
```

如果我们幸运的话，该注释对应于工作计划中的函数目标描述。

下一步是确定参数和返回值。 同样，参数的名称经常告诉我们很多，但不幸的是，Python 通常没有明确的参数类型（它们不会被 Python 检查）所以我们必须自己解决这个问题。 了解值和变量的类型对于理解程序至关重要。 在这样的简单函数中，我们通常可以快速找出参数的类型和返回值。 在其他情况下，我们将不得不深入研究函数的语句来解决这个问题（稍后会详细介绍）。 让我们放大来查看我们函数的更多细节：
 
```python
def average(data):
    ...
    return sum / n
```

在这一点上，我们知道`data`几乎肯定是一个数字列表，函数返回一个数字。 这意味着我们可以填写该功能的工作计划的第一部分。

## 在函数代码中寻找什么

因为我们事先知道平均值是什么，所以我们可以填写函数目标的工作计划描述。 但是，一般来说，我们必须扫描函数的语句才能弄明白。 （我们可能会很幸运并找到合理的函数注释。）现在让我们看一下完整的函数：

```python
def average(data):
    n = len(data)
    sum = 0.0
    for x in data:
        sum = sum + x
    return sum / n
```

缺乏经验的程序员必须单独和逐字地检查函数的语句，模拟计算机来找出突现行为。 相比之下，*经验丰富的程序员在代码中寻找模式，代表映射，搜索，过滤等高级操作的实现*.....

通过类比，考虑在游戏过程中记住棋盘的状态。 初学者必须单独记住所有东西在哪儿，而国际象棋大师则认为棋盘只是布达佩斯开局的变种。

我们如何知道从哪里开始以及看什么？ 那么，让我们回想一下我们的通用数据科学程序模板：

1.  获取数据，这意味着找到合适的文件或从 Web 收集数据并存储在文件中
2.  从磁盘加载数据并放入组织成数据结构的内存中
3.  规范，清理或以其他方式准备数据
4.  处理数据，这可能意味着训练机器学习模型，计算汇总统计量或优化成本函数
5.  输入结果，可以是任何东西，从简单地打印答案，到将数据保存到磁盘，以及生成花哨的可视化

该过程的要点是，将数据加载到方便的数据结构中并对其进行处理。加载数据，创建数据结构和处理数据结构有什么共同之处？它们都重复执行一组操作，这意味着处理数据的程序的要点是循环。（甚至有一本着名的书名为[算法+数据结构=程序](https://www.amazon.com/Algorithms-Structures-Prentice-Hall-Automatic-Computation/dp/0130224189)，其中*算法*表示伪代码或代码描述的过程。）没有循环的程序可能会非常无聊，因为它无法遍历数据结构或处理数据文件。

从这里，我们可以得出结论，所有的动作都发生在循环中，所以我们应该首先在代码中寻找循环**。阅读代码是在函数代码中找到这样的模板的问题，它立即告诉我们作者想要的操作或模式的类型。

## 识别代码中的编程模式

让我们深入研究一些循环示例，尝试识别高级模式和相应的操作。 要寻找的关键要素是我们研究的模板中的空位。 这通常意味着识别循环变量，循环边界，我们正在遍历的数据结构以及对数据元素执行的操作。**目标是对代码作者的意图进行逆向工程。**

**练习**：首先，上面的`sum`函数中的代码模式的对应操作是什么？

```python
sum = 0.0
for x in data:
    sum = sum + x
```

那是一个累积器。

**练习**：让我们看一个循环，我故意使用蹩脚的变量名称，所以你必须专注于功能。

```python
foo = []
for blah in blort:
    foo.append(blah * 2)
```

这是一个映射操作，我们可以从空目标列表的初始化和`foo.append(...)`调用中看到。 除了目标列表是`blah`的函数，它来自源列表`blort`之外，`blah * 2`与寻找模式无关。

**练习**：你在下面的代码中看到了什么样的循环（`for-each`，索引，嵌套等等）？ 代码执行什么样的高级操作？

```python
blort = []
for boo in range(len(foo)):
    blort.append(foo[boo] * 2)
```

这是一个索引循环，它再次执行映射操作。 它是一个索引循环的线索是，边界是`range(len(foo))`，它给出一系列索引。 由于`blort.append`和`foo[boo]`的引用，我们知道它是一个映射操作。 因为`[boo]`索引运算符，我们知道`foo`是某种类型的列表。

**练习**：对应此代码中模式的高级操作是什么：

```python
foo = []
for i in range(len(X)):
    foo.append(X[i]+Y[i])
```

它将两列（列表）组合成目标列/列表`foo`。我们知道`X`和`Y`是列表，因为`[i]`数组索引。

**练习**：此代码执行什么高级数学运算？

```python
for i in range(n):
    for j in range(n):
        C[i][j] = A[i][j] + B[i][j]
```

矩阵加法。这里重要的是要认识到，嵌套的索引循环给出了在`[0..n]`范围内的循环变量`i`和`j`的所有组合。 执行此操作的最常见原因之一是迭代矩阵或图像的元素。 这里的答案也可能是图像加法。

**练习**：这个循环打印了多少个`hi`？

```python
for i in range(n):
    for j in range(n):
        print('hi')
```

`n * n`。内循环`n`次。外循环意味着我们执行整个内循环`n`次。

**练习**：

```python
blort = []
for foo in A:
    for bar in B:
        blort.append(foo + bar)
```

这从`A`和`B`的所有可能组合中找到所有情况。

**练习**：这段代码在做什么？ 即，循环完成后，`blort`的值是多少？

```python
blort = float('-inf')
for x in X:
    if x > blort:
        blort = x
print(blort)
```

`X`的最大值。

<img src="img/redbang.png" width="30" align="left">

无论何时在循环内部看到`if`语句，请考虑*过滤*或*搜索*或*条件累积*。 它通常是其中一个的变体。这假设条件表达式是直接或间接的循环变量的函数。

**练习**：这个变体打印了什么？

```python
blort = float('-inf')
for i in range(len(X)):
    if X[i] > blort:
        blort = X[i]
print blort
```

完全一样的东西; `blort`是`X`的最大值。 您会看到一个条件表达式，它是循环内部循环变量的函数。这只是前一次的重组。

**练习**：这段代码的目标是什么？ 即，循环后它为`foo`打印的值是多少？

```python
foo = -1
bar = -99999
for i in range(len(X)):
    if X[i] > bar:
        bar = x
        foo = i
print(foo)
```

`X`的最大值索引（`argmax`）。 我们知道与条件相关的代码，是从前面的例子中找出最大值，但它也跟踪了索引`i`。

可以把它想象成你已经想到的标准模式，但这种变体可以做一些额外的事情。 然后问两者之间有什么区别。

这是尝试理解输入 - 输出对是什么的一个很好的例子（虽然我们在这里谈论的是代码段而不是完整的函数）。 在最大值的计算中，输出是取自`X`的值。 在这种情况下，打印出的值是`0..len(X)-1`中的索引。

**练习**：描述此代码完成后`bar`的值。

```python
foo = []
bar = []
for blah in blort:
    foo.append(blah * 2)
for zoo in foo:
    if zoo>10:
        bar.append(zoo)
```

这里有很多东西，但它实际上只不过是一个序列中的两个模式。 第一个模式是一个映射操作，它将`blort`中的值加倍，来创建`foo`列表，该列表由第二个循环使用。第二个循环只是一个过滤，它将所有`> 10`的值从`foo`提取到`bar`。

**练习**：执行此代码后，`a`和`b`是什么值？

```python
a = 0
b = 0
for x in X:
    if x < 10:
        a = a + 1
    else:
        b = b + 1
```

这是一个具有条件的双重累积。 它是一个累积，因为它在循环中更新至少一个变量。 它有一个累积条件，因为它是一个累积循环中的条件，其中条件表达式测试循环迭代器的值。 `a`是`X`中小于 10 的值的数量，`b`是大于或等于 10 的值的数量。

**练习**：循环后`Y`是什么值？

```python
a = 2
b = 5
Y = []
for i in range(len(X)):
    if i>=a and i<=b:
        Y.append(X[i])
```

此循环实现切片操作，从列表中提取元素的子集。 在这种情况下，它选择范围`a..b`中的`X`的元素，包含边界，并将它们添加到`Y`。

该实现效率非常低，因为它遍历整个列表来获取范围内的元素。 如果我们将循环的边界更改为所需范围，它会更快更容易理解：

```python
a = 2
b = 5
Y = []
for i in range(a, b+1):  # range is [a,b]
    Y.append(X[i])
```

## 总结

编写代码是成为程序员的重要部分。代码是程序员的沟通方式。 这是我们有效地使用额外的库，调试，以及获得经验的方式。

阅读代码的技巧是翻转从函数工作计划到伪代码再到 Python 代码的编程过程。 最大的线索来自变量和函数名称，可能还有代码注释。 然后，我们查找代码中所表达的代码模板，根据该模板的选择，对作者的原始意图进行反向工程。 例如，询问代码代表映射还是搜索操作。 不要试图模仿计算机，并使用表达式值和时间的图表来猜测突现行为。 有时你必须这样做才能进行调试，但总的来说，你的目标是猜测代码作者的意图。

因为阅读代码是您流程的重要组成部分，所以通过编写高质量的代码，善待其他开发人员和您未来的自已。 这包括选择优秀的变量和函数名称以及编写清楚说明您意图的代码。
