来源: ApacheCN『USF MSDS501 计算数据科学中文讲义』翻译项目原文:How to read codepython
译者:飞龙c++
协议:CC BY-NC-SA 4.0git
从根本上说,程序员与代码交流。咱们不只向计算机,也向其余开发人员表达了咱们的想法。到目前为止,咱们专一于设计程序和编写 Python 代码。这是关键的创做过程,可是,为了编写代码,程序员必须可以阅读其余人编写的代码。程序员
咱们阅读代码以便:github
<img src="https://gitee.com/wizardforce...; width="30" align="left">算法
在咱们讨论库函数时,让我强调一条黄金法则:你永远不该该向你的程序员询问参数的细节和库函数的返回值。你能够经过 PyCharm 中的“跳转到定义”或网络搜索来本身发现它。apache
本文档的目的是解释程序员如何读取代码。 咱们的第一个线索来自于咱们不是计算机这一事实,所以,咱们不该该像计算机同样阅读代码,一个接一个地检查一个符号。 相反,咱们将寻找关键元素和代码模式。编程
这就是咱们用外语阅读句子时所作的事情。 例如,个人法语很是糟糕,所以,在阅读法语句子时,我必须有意识地询问谁在对谁作什么。在实践中,这意味着识别主语,动词和宾语。从这些关键要素中,我试图想象做者心中的思惟模式。基本上我试图反转做者所遵循的过程。数组
在编程世界中,过程以下:代码做者可能会想到“经过除以 2 将价格转换为新列表”,而后将它们转换为“映射”的伪代码,最后转换为 Python for
循环。在阅读循环代码时,咱们的工做是反转过程,并想象做者的原始目标。 咱们不是试图经过在咱们的头脑或纸上模拟它,来弄清楚代码的突现行为;相反,咱们正在寻找模式,它们可以告诉咱们正在执行哪些高级操做。网络
这就是为何在编写代码时应该强调清晰度,以便读者阅读更多内容。约翰 F. 伍兹 有一个很好的引言,总结了不少东西:
写代码的时候老是想象,维护你代码的家伙是一个知道你住在哪里的暴力精神病患者。
在第一次查看教科书时,扫描目录来得到书籍内容的总体视图,是有意义的。 第一次看节目时也是如此。 查看全部文件以及这些文件中包含的函数的名称。 同时,找出主程序的位置。 根据您在程序中的目标,您可能会开始单步执行主程序或当即跳转到感兴趣的函数。
从样例运行或单元测试中查看程序的输入 - 输出对也颇有用,由于它能够帮助您了解程序的功能。 从某种意义上说,咱们经过检查和测试程序,对程序的工做计划进行逆向工程。 之前,咱们在前进方向使用程序的工做计划来设计程序。
一旦咱们肯定了要检查的主程序或函数,就应该对函数的工做计划进行反向工程。 函数的名称多是函数功能的最大线索,假设代码做者是一个不错的程序员。 (使用像f
这样的通用函数名称,是教师在不泄露答案的状况下,编写代码阅读问题的方式。)例如,毫无疑问,如下函数的目标是什么:
def average(...): ...
即便不查看参数或函数语句。
程序员一般会提供函数用法的注释,但要当心。 程序员一般会在不更改注释的状况下更改代码,所以注释会产生误导。可接受的注释可能以下所示:
def average(...): "Compute and return the average of a list of numbers" ...
若是咱们幸运的话,该注释对应于工做计划中的函数目标描述。
下一步是肯定参数和返回值。 一样,参数的名称常常告诉咱们不少,但不幸的是,Python 一般没有明确的参数类型(它们不会被 Python 检查)因此咱们必须本身解决这个问题。 了解值和变量的类型对于理解程序相当重要。 在这样的简单函数中,咱们一般能够快速找出参数的类型和返回值。 在其余状况下,咱们将不得不深刻研究函数的语句来解决这个问题(稍后会详细介绍)。 让咱们放大来查看咱们函数的更多细节:
def average(data): ... return sum / n
在这一点上,咱们知道data
几乎确定是一个数字列表,函数返回一个数字。 这意味着咱们能够填写该功能的工做计划的第一部分。
由于咱们事先知道平均值是什么,因此咱们能够填写函数目标的工做计划描述。 可是,通常来讲,咱们必须扫描函数的语句才能弄明白。 (咱们可能会很幸运并找到合理的函数注释。)如今让咱们看一下完整的函数:
def average(data): n = len(data) sum = 0.0 for x in data: sum = sum + x return sum / n
缺少经验的程序员必须单独和逐字地检查函数的语句,模拟计算机来找出突现行为。 相比之下,经验丰富的程序员在代码中寻找模式,表明映射,搜索,过滤等高级操做的实现.....
经过类比,考虑在游戏过程当中记住棋盘的状态。 初学者必须单独记住全部东西在哪儿,而国际象棋大师则认为棋盘只是布达佩斯开局的变种。
咱们如何知道从哪里开始以及看什么? 那么,让咱们回想一下咱们的通用数据科学程序模板:
该过程的要点是,将数据加载到方便的数据结构中并对其进行处理。加载数据,建立数据结构和处理数据结构有什么共同之处?它们都重复执行一组操做,这意味着处理数据的程序的要点是循环。(甚至有一本着名的书名为算法+数据结构=程序,其中算法表示伪代码或代码描述的过程。)没有循环的程序可能会很是无聊,由于它没法遍历数据结构或处理数据文件。
从这里,咱们能够得出结论,全部的动做都发生在循环中,因此咱们应该首先在代码中寻找循环**。阅读代码是在函数代码中找到这样的模板的问题,它当即告诉咱们做者想要的操做或模式的类型。
让咱们深刻研究一些循环示例,尝试识别高级模式和相应的操做。 要寻找的关键要素是咱们研究的模板中的空位。 这一般意味着识别循环变量,循环边界,咱们正在遍历的数据结构以及对数据元素执行的操做。目标是对代码做者的意图进行逆向工程。
练习:首先,上面的sum
函数中的代码模式的对应操做是什么?
sum = 0.0 for x in data: sum = sum + x
那是一个累积器。
练习:让咱们看一个循环,我故意使用蹩脚的变量名称,因此你必须专一于功能。
foo = [] for blah in blort: foo.append(blah * 2)
这是一个映射操做,咱们能够从空目标列表的初始化和foo.append(...)
调用中看到。 除了目标列表是blah
的函数,它来自源列表blort
以外,blah * 2
与寻找模式无关。
练习:你在下面的代码中看到了什么样的循环(for-each
,索引,嵌套等等)? 代码执行什么样的高级操做?
blort = [] for boo in range(len(foo)): blort.append(foo[boo] * 2)
这是一个索引循环,它再次执行映射操做。 它是一个索引循环的线索是,边界是range(len(foo))
,它给出一系列索引。 因为blort.append
和foo[boo]
的引用,咱们知道它是一个映射操做。 由于[boo]
索引运算符,咱们知道foo
是某种类型的列表。
练习:对应此代码中模式的高级操做是什么:
foo = [] for i in range(len(X)): foo.append(X[i]+Y[i])
它将两列(列表)组合成目标列/列表foo
。咱们知道X
和Y
是列表,由于[i]
数组索引。
练习:此代码执行什么高级数学运算?
for i in range(n): for j in range(n): C[i][j] = A[i][j] + B[i][j]
矩阵加法。这里重要的是要认识到,嵌套的索引循环给出了在[0..n]
范围内的循环变量i
和j
的全部组合。 执行此操做的最多见缘由之一是迭代矩阵或图像的元素。 这里的答案也多是图像加法。
练习:这个循环打印了多少个hi
?
for i in range(n): for j in range(n): print('hi')
n * n
。内循环n
次。外循环意味着咱们执行整个内循环n
次。
练习:
blort = [] for foo in A: for bar in B: blort.append(foo + bar)
这从A
和B
的全部可能组合中找到全部状况。
练习:这段代码在作什么? 即,循环完成后,blort
的值是多少?
blort = float('-inf') for x in X: if x > blort: blort = x print(blort)
X
的最大值。
<img src="https://gitee.com/wizardforce...; width="30" align="left">
不管什么时候在循环内部看到if
语句,请考虑过滤或搜索或条件累积。 它一般是其中一个的变体。这假设条件表达式是直接或间接的循环变量的函数。
练习:这个变体打印了什么?
blort = float('-inf') for i in range(len(X)): if X[i] > blort: blort = X[i] print blort
彻底同样的东西; blort
是X
的最大值。 您会看到一个条件表达式,它是循环内部循环变量的函数。这只是前一次的重组。
练习:这段代码的目标是什么? 即,循环后它为foo
打印的值是多少?
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
的值。
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
是什么值?
a = 0 b = 0 for x in X: if x < 10: a = a + 1 else: b = b + 1
这是一个具备条件的双重累积。 它是一个累积,由于它在循环中更新至少一个变量。 它有一个累积条件,由于它是一个累积循环中的条件,其中条件表达式测试循环迭代器的值。 a
是X
中小于 10 的值的数量,b
是大于或等于 10 的值的数量。
练习:循环后Y
是什么值?
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
。
该实现效率很是低,由于它遍历整个列表来获取范围内的元素。 若是咱们将循环的边界更改成所需范围,它会更快更容易理解:
a = 2 b = 5 Y = [] for i in range(a, b+1): # range is [a,b] Y.append(X[i])
编写代码是成为程序员的重要部分。代码是程序员的沟通方式。 这是咱们有效地使用额外的库,调试,以及得到经验的方式。
阅读代码的技巧是翻转从函数工做计划到伪代码再到 Python 代码的编程过程。 最大的线索来自变量和函数名称,可能还有代码注释。 而后,咱们查找代码中所表达的代码模板,根据该模板的选择,对做者的原始意图进行反向工程。 例如,询问代码表明映射仍是搜索操做。 不要试图模仿计算机,并使用表达式值和时间的图表来猜想突现行为。 有时你必须这样作才能进行调试,但总的来讲,你的目标是猜想代码做者的意图。
由于阅读代码是您流程的重要组成部分,因此经过编写高质量的代码,善待其余开发人员和您将来的自已。 这包括选择优秀的变量和函数名称以及编写清楚说明您意图的代码。