编写科学代码时干净编程


169

我不是真的写大型项目。我不是在维护庞大的数据库,也不是在处理数百万行代码。

我的代码主要是“脚本”类型的东西-测试数学功能或模拟某些东西的东西-“科学编程”。到目前为止,我从事的最长程序是几百行代码,而我从事的大多数程序都在150左右。

我的代码也很烂。前几天,当我试图找到我之前写的一个文件时,我意识到了这一点,但是我可能改写了,并且我没有使用版本控制,这可能使大量的您对我的愚蠢感到痛苦。

我的代码风格令人费解,并填充了过时的注释,这些注释指出了执行某项操作的替代方法或复制了几行代码。尽管变量名总是以非常好的描述性开始,但是当我按照某人想要测试的新内容进行添加或更改时,代码却被覆盖在顶部并被覆盖,因为我觉得这件事应该立即进行测试。有一个框架,我开始使用糟糕的变量名,并且文件进入了pot。

在我现在正在从事的项目中,我正处在所有这些阶段都重新吸引我的阶段。但是问题是(除了使用版本控制,以及为每个新迭代创建一个新文件并将其全部记录在某个地方的文本文件中,这可能会极大地改善这种情况)我真的不知道如何进行改进我实际的编码风格。

编写较小的代码是否需要单元测试?OOP怎么样?相对于处理较大的项目,什么样的方法适合在进行“科学编程”时快速编写优质,清晰的代码?

我之所以问这些问题,是因为编程本身通常并不复杂。它更多地是关于我正在通过编程测试或研究的数学或科学。例如,当两个变量和一个函数可能需要处理时,是否需要一个类?(考虑到一般情况下,程序的速度也应优先考虑更快的情况-当您运行25,000,000+个仿真时间步长时,您一定会希望它是这样。)

也许范围太广了,如果是的话,我对此表示歉意,但是从编程书籍的角度来看,它们似乎经常在大型项目中得到解决。我的代码不需要 OOP,而且已经很短了,所以它不像“哦,但是如果这样做的话,文件将减少一千行!” 我想知道如何在这些较小,更快的项目上“重新开始”并进行干净的编程。

我很乐意提供更多具体细节,但是我认为建议越笼统,就越有用。我正在用Python 3编程。


有人建议重复。让我明确地说,我并不是在谈论完全忽略标准编程标准。显然,存在这些标准是有原因的。但是,另一方面,如果可以完成一些标准的工作,写出说OOP的代码真的有意义吗?程序?

有例外。此外,除了简单的标准外,可能还有科学编程的标准。我也在问那些。这与编写科学代码时是否应忽略常规编码标准无关,而与编写干净的科学代码有关!


更新资料

只是以为我会添加“不超过一个星期以后”的更新。您的所有建议都非常有帮助。我现在使用版本控制-git,其中git kraken用于图形界面。它非常易于使用,并且彻底清理了我的文件-不再需要保留旧文件,也不再需要旧版本的代码“以防万一”。

我还安装了pylint并在所有代码上运行了它。最初,一个文件的分数为负。我什至不知道那怎么可能。我的主文件开始于〜1.83 / 10,现在为〜9.1 / 10。现在,所有代码都非常符合标准。我还用自己的眼睛跑过去,更新了已经消失的变量名,嗯,很糟糕,正在寻找要重构的部分。

特别是,我在这个网站上最近问了一个有关重构我的主要功能的问题,它现在更加整洁和短了很多:它不再是一个冗长,肿的if / else填充功能,现在不到一半的大小,更容易弄清楚发生了什么。

我的下一步是实施各种“单元测试”。我的意思是一个可以在主文件上运行的文件,该文件使用assert语句和try / excepts来查看其中的所有功能,这可能不是最好的方法,并且会导致重复的代码很多,但我会继续阅读并尝试找出如何做得更好。

我还对我已经编写的文档进行了重大更新,并向github存储库添加了补充文件,例如excel电子表格,文档和相关论文。现在看起来有点像一个真正的编程项目。

所以...我想这就是要说的:谢谢




8
如果您想积极地改进代码,则可以考虑在Code Review上发布一些内容。那里的社区将很乐意为您提供帮助。
hoffmale

7
当您说“除了使用版本控制,并为每个新迭代创建一个新文件并将其全部记录在某个文本文件中的某个位置时”时,用“和”表示“或”,因为如果使用版本控制,则不不能复制粘贴版本。关键是版本控制为您保留了所有旧版本
Richard Tingle '18

2
@mathreadler我认为您不太了解。是的,只有我可能会真正阅读并弄乱代码(尽管您永远不知道,而且我确实有一个与我合作的人可以编程,尽管使用的是不同的语言)...但是代码仍然废话 我将不得不稍后阅读并再次弄清楚我在做什么。这一个问题,我可以作证,因为我现在正在体验这种效果,并且由于实现了版本控制和此处建议的其他技术,事情变得更容易了。
希瑟

Answers:


163

对于科学家来说,这是一个非常普遍的问题。我已经看过很多了,它总是源于这样一个事实,那就是编程是您可以选择用来完成工作的工具。

因此,您的脚本很乱。我要违背常识说,假设您是独自编程,那还不错!您再也不会接触到您写过的大部分内容,因此,花费太多时间来编写漂亮的代码而不是产生“值”(因此,脚本的结果)对您没有多大作用。

但是,有时您需要回到所做的事情,并确切地了解工作原理。此外,如果其他科学家需要审查您的代码,那么尽可能清晰简洁也很重要,这样每个人都可以理解它。

您的主要问题将是可读性,因此,这里有一些改进的技巧:

变量名:

科学家喜欢使用简洁的符号。所有数学方程式通常都使用单个字母作为变量,并且在您的代码中看到很多非常短的变量也就不足为奇了。这极大地损害了可读性。当您返回到代码时,您将不会记住y,i和x2所代表的含义,并且您将花费大量时间来弄清楚它。尝试使用确切表示变量名的变量来明确命名变量。

将代码拆分为功能:

现在,您已重命名了所有变量,方程式看起来很糟糕,并且有多行。

不要将该方程式留在您的主程序中,而是将其移动到其他函数中,并相应地命名。现在,您将不再需要编写大量混乱的代码,而是使用简短的说明,告诉您正在发生的事情以及所使用的方程式。这不仅可以改善您的主程序,还可以改善您的主程序,因为您甚至不必查看实际方程式就知道自己做了什么,而方程式代码本身也可以,因为在单独的函数中,您可以根据需要命名变量,然后返回比较熟悉的单个字母。

按照这种思路,尝试找出代表某物的所有代码段,尤其是当某物是您必须在代码中多次执行的事情时,并将其拆分为功能。您会发现您的代码将很快变得易于阅读,并且无需编写更多代码就可以使用相同的功能。

锦上添花,如果在您的更多程序中需要这些功能,则可以为其建立一个库,并使它们始终可用。

全局变量:

当我还是一个初学者时,我认为这是传递程序许多点所需数据的好方法。事实证明,还有很多其他方法可以传递信息,而全局变量所做的唯一事情就是让人头疼,因为如果您进入程序的随机位置,您将永远不知道该值的最后使用或编辑时间,并且追踪它会很痛苦。尽可能避免它们。

如果您的函数需要返回或修改多个值,则可以使用这些值创建一个类并将其作为参数传递下来,或者使函数返回多个值(具有命名元组)并在调用者代码中分配这些值。

版本控制

这并不能直接提高可读性,但是可以帮助您完成上述所有操作。每当您进行一些更改时,请进行版本控制(本地Git存储库就足够了),如果某些操作不起作用,请查看您所做的更改或回滚!这将使重构代码的方式变得更加容易,并且如果您不小心破坏了某些内容,将是一个安全网。

牢记所有这些,将使您可以编写清晰,更有效的代码,并且还可以帮助您更快地发现可能的错误,因为您不必费解巨大的函数和混乱的变量。


56
极好的建议。但是,我不能对“还不错”的评论表示赞同。太糟糕了 低质量的科学脚本是数据分析可重复性的主要问题,也是分析错误的常见来源。编写好的代码不仅很有价值,以便以后您可以理解它,而且也可以避免一开始就出错。
杰克·艾德利

22
如果代码是众所周知的公式的实现,则单字母变量名称等可能是正确的做法。取决于听众...更重要的是,是否希望读者已经知道名称的含义。
cHao

11
@cHao问题不是在公式中插入了这些变量(因此“在函数内将其重命名”建议),而是在变量外对其进行读取和操作时以及当它们开始与其他变量冲突时(例如,我见过需要三个“ x”变量的人将它们命名为x1,x2,x3)
BgrWorker

4
“我会违背常识……”不,你不是。您要违背普遍的教条,这本身就是违反常识的。;)这都是完美的建议。
jpmc26

3
作为(前)科学程序员,我通常采用三个规则。如果我最后写了三遍类似的代码,则该功能会充实并写在带有文档的单独模块中(通常只是注释,但这足够了)。这限制了即席编程的混乱,并允许我建立一个将来可以扩展的库。
rcollyer '18

141

物理学家在这里。到过那里。

我认为您的问题不在于工具或编程范例的选择(单元测试,OOP等)。这是关于 态度和心态。您的变量名一开始就经过精心选择,最终却被胡扯了,这一事实充分说明了这一点。如果您认为您的代码“运行一次,然后扔掉”,那将不可避免地是一团糟。如果您将其视为手工艺和爱情的产物,它将会是美丽的。

我相信只有一种编写清晰代码的方法:为将要阅读的人编写代码,而不是为将要运行它的解释器编写代码。解释器不会在乎您的代码是否一团糟,但人类读者在乎。

你是科学家。您可能会花费大量时间来整理科学文章。如果您的初稿看起来很复杂,您将对其进行重构,直到逻辑以最自然的方式流动为止。您希望您的同事阅读它并找到清晰的论点。您希望学生能够从中学到东西。

编写干净的代码是完全一样的。将您的代码视为对算法的详细解释,该算法只是偶然地是机器可读的。假设您将其发布为人们可以阅读的文章。您甚至将在会议上展示它,并逐行引导观众。现在排练您的演示文稿。是的,逐行!不好意思,不是吗?因此,请整理幻灯片(错误,我的意思是您的代码),然后再排练。重复直到对结果满意为止。

在排练之后,如果您可以将代码展示给真实的人,而不只是虚构的人和未来的自我,那还是更好。逐行进行此操作称为“代码遍历”,这不是一个愚蠢的做法。

当然,所有这些都是有代价的。编写干净的代码需要很多 比编写一次性的代码更多的时间。只有您可以评估您的特定用例的收益是否超过成本。

至于工具,我之前说过它们并不那么重要。但是,如果必须选择一种,我会说版本控制是最有用的。


32
“写清晰的代码与写清晰的文章完全一样。”我完全赞同这一点,说得好!
juandesant

43
这是大多数专业程序员都忘记说的东西,因为它是如此明显。当代码可以被其他程序员读取和修改时,您的代码即告完成。不能在运行时产生正确的输出。OP需要花费每个脚本花费额外的时间来重构和注释其代码,以使其易于阅读。
UEFI

31
尽管编写干净的代码确实比编写一次性代码花费更多的时间,但更重要的是,读取一次性代码比读取干净的代码要花费更多的时间。
user949300 '18

3
@UEFI不,大多数专业程序员都没有意识到这一点。还是不在乎。
jpmc26

2
同意100%。统计学家变成了程序员,所以我在工作中做了很多“科学”编程。当您必须在1、4或12个月后返回该代码时,清晰的代码以及有意义的注释将是您的救命稻草。阅读代码可以告诉您代码在做什么。阅读注释会告诉您代码应该执行的操作。
railsdog '18

82

版本控制可能会为您带来最大的收益。它不仅用于长期存储,而且对跟踪您的短期实验并返回到上一个有效的版本非常有用,并始终保持记录。

接下来最有用的是单元测试。关于单元测试的事情是,甚至具有数百万行代码的代码库一次都被单元测试了一个功能。单元测试是在最小的最低抽象级别上完成的。这意味着为小型代码库编写的单元测试与用于大型代码库的单元测试在根本上没有区别。有更多。

单元测试是避免破坏您在修复其他东西时已经起作用的东西的最佳方法,或者至少是告诉您什么时候做的最好方法。当您不像程序员那样熟练时,或者不知道如何或不想编写更多冗长的代码以使错误的可能性降低或变得更加明显时,它们实际上会更有用。

在进行版本控制和编写单元测试之间,您的代码自然会变得更加干净。当您达到平稳状态时,可以学习其他更清晰的编码技术。


78
我虔诚地对我的大部分代码进行单元测试,但发现单元测试是探索性的,科学的代码也没什么用。从根本上讲,该方法似乎不起作用。我不认识该领域中的任何计算科学家对他们的分析代码进行单元测试。我不知道是什么造成这种不一致的原因,但一个原因肯定的是,除了微不足道的单位,这是很难或不可能建立良好的测试用例。
康拉德·鲁道夫'18

22
在这些情况下,@ KonradRudolph的诀窍可能是将代码中具有明确定义的行为(读取此输入,计算该值)的部分与真正探究的或正在适应例如一些人类可读的输出或可视化。这里的问题很可能是,关注点之间的不良分离会导致这些线的模糊,从而导致人们认为在这种情况下不可能进行单元测试,这会使您回到重复周期的起点。
Ant P

18
顺便说一句,由于LaTeX文档的格式易于进行文本区分,因此版本控制也非常适用于LaTeX文档。这样,您可以为您的论文和支持它们的代码建立一个存储库。我建议研究分布式版本控制,例如Git。学习曲线有些许变化,但是一旦您了解了它,便有了一种很好的简洁方法来迭代您的开发,并且您可以使用一些有趣的选项来使用类似Github的平台,该平台为学术人员提供免费的团队帐户
丹·布莱恩特'18

12
@AntP可能没有太多代码可以有意义地重构为定义良好的可测试单元。本质上,许多科学代码正在将一堆库集中在一起。这些库已经过了良好的测试,结构清晰,这意味着作者仅需编写“胶水”,以我的经验,几乎不可能编写非重言式的胶水单元测试。
James_pic '18年

7
“在进行版本控制和编写单元测试之间,您的代码自然会变得更加干净。” 这是不对的。我可以亲自证明。这两种工具都不能阻止您编写糟糕的代码,特别是在糟糕的代码之上编写糟糕的测试只会使清理工作变得更加困难。测试不是万能的灵丹妙药,对于任何仍在学习的开发人员(每个人)来说,像进行测试一样进行交谈是一件可怕的事情。但是,版本控制通常不会像不良测试那样对代码本身造成损害。
jpmc26

29

(除了使用版本控制,并为每个新迭代创建一个新文件并将其全部记录在某个地方的文本文件中,这可能会极大地帮助解决这种情况)

您可能会自己想出来的,但是如果您必须“ 将所有内容都记录在一个文本文件中 ”,则您并没有充分利用版本控制系统的潜力。使用Subversion,git或Mercurial之类的东西,并在每次提交时写一个好的提交消息,您将获得一个日志,该日志用于文本文件,但不能与存储库分开。

除此之外,使用版本控制是您可以做的最重要的事情,原因是现有答案都没有提到:结果的可重复性。如果您既可以使用日志消息,也可以在结果中添加带有修订号的注释,则可以确保能够重新生成结果,并且最好将其随论文一起发布。

编写较小的代码是否需要单元测试?OOP怎么样?相对于处理较大的项目,什么样的方法适合在进行“科学编程”时快速编写优质,清晰的代码?

单元测试从来都不是必需的,但如果(a)代码足够模块化,可以测试单元而不是整个单元,则它会很有用;(b)您可以创建测试。理想情况下,您可以手动编写预期的输出,而不用代码生成它,尽管通过代码生成它至少可以为您提供回归测试,该测试可以告诉您某些事情是否改变了其行为。只需考虑测试是否比正在测试的代码更容易出错。

OOP是一种工具。如果有帮助,请使用它,但这不是唯一的范例。我假设您只真正了解过程编程:如果是这种情况,那么在所描述的上下文中,我认为与OOP相比,您可以从研究函数式编程中受益更多,尤其是避免可能出现的副作用的学科。Python可以以非常实用的风格编写。


4
+1提交消息;它们就像不能过时的注释,因为它们在实际适用时与代码版本相关。要了解您的旧代码,浏览项目的历史记录(如果更改以合理的粒度提交)比阅读过时的注释要容易得多。
Silly Freak

Subversion,git和Mercurial不可替代。我强烈建议在Subversion的本地存储库中使用Git(或Mercurial)。使用单独的编码器,Subversion的缺陷已不再是问题,但它并不是协作开发的好工具,并且可能在研究中发生
mcottle

2
@mcottle,我个人更喜欢git,但是我不认为这是详细讨论差异的地方,特别是因为选择是一场活跃的宗教战争之一。鼓励OP使用某些东西要比吓跑他们远离该地区更好,而且这种决定在任何情况下都不是永久的。
彼得·泰勒,

21

在研究生院,我自己写了一些算法繁重的代码。这有点难以破解。粗略地说,许多编程约定都是围绕以下思想进行的:将信息放入数据库,在正确的时间检索它,然后对该数据进行按摩以将其呈现给用户,通常使用库来进行任何数学运算或运算。该过程中算法繁重的部分。对于这些程序,您所听说的有关OOP的所有知识,将代码分解为短函数以及使所有内容一目了然易于理解的方法都是极好的建议。但是,对于算法繁重的代码或实现复杂数学计算的代码,它几乎不起作用。

如果您要编写脚本来执行科学计算,则可能会有论文写有您使用的方程式或算法。如果您使用的是自己发现的新想法,则希望将它们发布在自己的论文中。在这种情况下,规则是:您希望您的代码阅读尽可能像已发布的方程式。这是有关Software Engineering.SE的答案,有200多个投票支持这种方法并解释其效果:短变量名是否有借口?

再举一个例子,Simbody中有一些很棒的代码片段,Simbody是一种用于物理研究和工程的物理模拟工具。这些摘录带有注释,其中显示了用于计算的方程式,后跟代码,其读取的内容与所实现的方程式尽可能接近。

ContactGeometry.cpp

// t = (-b +/- sqrt(b^2-4ac)) / 2a
// Discriminant must be nonnegative for real surfaces
// but could be slightly negative due to numerical noise.
Real sqrtd = std::sqrt(std::max(B*B - 4*A*C, Real(0)));
Vec2 t = Vec2(sqrtd - B, -sqrtd - B) / (2*A);

ContactGeometry_Sphere.cpp

// Solve the scalar Jacobi equation
//
//        j''(s) + K(s)*j(s) = 0 ,                                     (1)
//
// where K is the Gaussian curvature and (.)' := d(.)/ds denotes differentiation
// with respect to the arc length s. Then, j is the directional sensitivity and
// we obtain the corresponding variational vector field by multiplying b*j. For
// a sphere, K = R^(-2) and the solution of equation (1) becomes
//
//        j  = R * sin(1/R * s)                                        (2)
//          j' =     cos(1/R * s) ,                                      (3)
//
// where equation (2) is the standard solution of a non-damped oscillator. Its
// period is 2*pi*R and its amplitude is R.

// Forward directional sensitivity from P to Q
Vec2 jPQ(R*sin(k * s), cos(k * s));
geod.addDirectionalSensitivityPtoQ(jPQ);

// Backwards directional sensitivity from Q to P
Vec2 jQP(R*sin(k * (L-s)), cos(k * (L-s)));
geod.addDirectionalSensitivityQtoP(jQP);

9
再加上一个用于使代码“尽可能地读取已发布的方程式的代码”。抱歉,建议使用长而有意义的变量名。科学代码中最有意义的名称通常是讨厌,简短和粗暴的,正是因为这正是该代码试图实现的科学期刊论文中所使用的约定。对于执行在期刊论文中找到的方程式的大量方程式代码,通常最好保持尽可能接近论文的术语,如果这与良好的编码标准背道而驰,那将是艰难的。
大卫·哈门

@DavidHammen:作为研究生,我尊重这一点。作为一名程序员,我坚持认为,每个函数的顶部都有一个巨大的注释块,以纯英语(或您选择的语言)描述每个变量的含义,即使只是一个临时占位符。这样,至少我可以参考一下。
tonysdg '18

1
@DavidHammen此外,Python支持源文件中的UTF-8和变量名称的简单规则,可以轻松声明λφ代替丑陋lambda_phy...
Mathias Ettinger

1
@tonysdg您已经有参考;称为“ Hammen等人(2018)”(或其他名称)。它将比任何注释块都更详细地解释变量的含义。保持变量名称与论文中的符号接近的原因恰恰是为了使其更容易将论文中的内容与代码中的内容联系起来。
没人

17

因此,我的日常工作是为加利福尼亚大学系统研究数据的发布和保存。有几个人提到可重复性,我认为这实际上是这里的核心问题:以记录其他人需要复制实验的方式记录代码,理想情况下,编写使其他人都容易理解的代码重现您的实验并检查结果是否有错误源。

但是我没有提到的,我认为很重要的一点是,资助机构越来越多地将软件发布作为数据发布的一部分,并使软件发布成为开放科学的要求。

为此,如果您想要针对研究人员而不是普通软件开发人员的特定内容,那么我不能高度推荐Software Carpentry组织。如果您可以参加他们的工作坊之一,那就太好了;如果您有时间/可以做的事情是阅读他们有关科学计算最佳实践的一些论文,那也很好。从后者:

科学家通常出于这些目的开发自己的软件,因为这样做需要大量特定领域的知识。结果,最近的研究发现,科学家通常花费30%或更多的时间来开发软件。但是,其中90%或更多的人主要是自学成才,因此缺乏基本软件开发实践的经验,例如编写可维护的代码,使用版本控制和问题跟踪器,代码审查,单元测试和任务自动化。

我们认为软件只是另一种实验设备,应该像任何物理设备一样仔细地构建,检查和使用软件。但是,尽管大多数科学家都在谨慎地验证他们的实验室和现场设备,但大多数人并不知道其软件的可靠性。这可能导致严重错误,影响已发表研究的主要结论。…

此外,由于软件通常用于多个项目中,并且经常被其他科学家重用,因此计算错误可能会对科学过程产生不成比例的影响。当直到发布后才发现来自另一个组的代码的错误时,这种级联的影响导致若干明显的撤回。

他们建议的做法的简要概述:

  1. 为人而不是为计算机编写程序
  2. 让计算机完成工作
  3. 进行增量更改
  4. 不要重复自己(或其他人)
  5. 计划错误
  6. 仅在软件正常运行后对其进行优化
  7. 文档设计和目的,而非机制
  8. 合作

本文对上述各点进行了相当详细的介绍。


16

如果可以完成一些标准的工作,编写写为OOP的代码真的有意义吗?由于程序的简短,编写代码的速度会快得多,并且具有相似的可读性?

个人回答:
我也出于科学目的编写了很多脚本。对于较小的脚本,我只是尝试遵循一般的良好编程习惯(即使用版本控制,使用变量名进行自我控制)。如果我只是在写一些东西来快速打开或可视化数据集,那么我就不会为OOP所困扰。

一般回答:
“取决于情况。” 但是,如果您想弄清楚何时使用编程概念或范例,则需要考虑以下几点:

  • 可扩展性:该脚本是否将独立存在,还是最终会在更大的程序中使用?如果是这样,是否使用OOP进行较大的编程?可以将脚本中的代码轻松集成到较大的程序中吗?
  • 模块化:通常,您的代码应该是模块化的。但是,OOP以一种非常特殊的方式将代码分成多个块。这种类型的模块化(即将脚本分解为类)是否对您的工作有意义?

我想知道如何在这些较小,更快的项目上“重新开始”并进行干净的编程。

#1:熟悉其中的内容:
即使您只是“脚本”脚本(您实际上只是在乎科学部分),您也应该花一些时间来学习不同的编程概念和范例。这样,您可以更好地了解应该/不应该使用什么以及何时使用。这听起来有些令人生畏。而且您可能仍然有一个问题,“我从哪里开始/我应该从哪里开始?” 我尝试在接下来的两个要点中解释一个很好的起点。

#2:开始解决您所知道的错误:就
我个人而言,我将从我所知道的错误开始。获得一些版本控制,并开始管教自己,以使用这些变量名变得更好(这是一个艰巨的努力)。解决您所知道的错误可能听起来很明显。但是,根据我的经验,我发现固定一件事会使我走向另一件事,依此类推。在不知不觉中,我揭露了我做错的10件事,并弄清楚了如何解决它们或如何以干净的方式实现它们。

#3:
寻找编程合作伙伴:如果您不打算“重新开始”,不参加正式的课程,请考虑与开发人员合作并要求他们检查您的代码。即使他们不了解您正在做的科学工作,他们也可能会告诉您您可以做些什么来使您的代码更优雅。

#4:寻找财团:
我不知道您所处的科学领域。但是,根据您在科学界的工作,尝试寻找财团,工作组或会议参与者。然后查看他们是否正在制定任何标准。这可能会导致您使用一些编码标准。例如,我做了很多地理空间工作。通过查看会议论文和工作组,我来到了开放地理空间联盟。他们要做的事情之一是制定地理空间开发标准。

希望对您有所帮助!


旁注:我知道您只是以OOP为例。我不想让您认为我在如何使用OOP编写代码方面陷入困境。继续回答该示例会更容易。


我认为#3是最重要的问题-经验丰富的程序员可以告诉OP他们需要的概念(#1),如何以更好的方式组织脚本以及如何使用版本控制(#2)。
布朗

16

我建议坚持Unix原则:保持简单,愚蠢!(吻)

或者,换一种说法:一次做一件事情,并做好。

这意味着什么?好吧,首先,这意味着您的功能应该简短。几秒钟之内无法完全理解其目的,用法和实现的任何功能肯定太长了。它可能同时执行多项操作,每项操作都应自己完成。所以把它分开。

就代码行而言,我的启发是10行是一个很好的功能,超过20行最有可能是废话。其他人有其他启发式方法。重要的是使长度尽可能短,您可以立即掌握。

如何拆分长函数?好吧,首先您要寻找重复的代码模式。然后,这些代码模式分解出来,给它们一个描述性的名称,并观察代码收缩。实际上,最好的重构是减少代码大小的重构。

当使用复制粘贴对相关功能进行编程时,尤其如此。每当您看到这样的重复模式时,便立即知道应该将其转变为它自己的功能。这是不重复自己(DRY)的原则。每当您遇到复制粘贴时,您就在做错事!而是创建一个函数。

轶事
我曾经花了几个月的时间重构代码,每个代码的功能大约为500行。完成后,总代码缩短了大约一千行;我在代码行方面产生了负输出。我欠公司(http://www.geekherocomic.com/2008/10/09/programmers-salary-policy/index.html)。尽管如此,我仍然坚信这是我做过的最有价值的作品之一...

某些功能可能会很长,因为它们会陆陆续续地做一些不同的事情。这些不是违反DRY的行为,但也可以拆分。结果通常是一个高级函数,该函数调用实现原始功能各个步骤的完整功能。通常,这将增加代码的大小,但是添加的函数名称在使代码更具可读性方面起作用。因为现在您有了一个顶层函数,其所有步骤都被明确命名。同样,在此拆分之后,可以清楚地知道哪个步骤对哪些数据进行操作。(函数参数。您不使用全局变量,对吗?)

当您试图编写一个段注释,或者在代码中找到一个段注释时,进行此类段函数拆分的一种很好的启发方法。这很可能是应该拆分功能的要点之一。本节的注释也可以用来激发新功能的名称。

KISS和DRY原则可以带您走很长一段路。您不需要立即开始使用OOP等,通常您只需应用这两种方法就可以简化工作。但是,从长远来看,了解OOP和其他范例确实会有所收获,因为它们为您提供了额外的工具,您可以使用这些工具来使程序代码更清晰。

最后,记录每个动作并提交。您将某些因素分解到一个新函数中,即commit。您将两个函数融合为一个,因为它们确实做同样的事情,那就是commit。如果重命名变量,则为commit经常提交。如果一天过去了,但是您没有承诺,那么您可能做错了什么。


2
关于拆分长方法的要点。关于轶事之后第一段的另一个很好的启发式方法:如果您的方法可以在逻辑上划分为多个部分,并且您很想写一条注释来解释每个部分的功能,则应在注释处将其分开。好消息,这些注释可能使您对如何调用新方法有了一个很好的了解。
Jaquez

@Jaquez Ah,完全忘记了那个。谢谢你的提醒。我已经更新我的答案,以包括此内容:-)
cmaster

1
要点,我想简单地说一下“ DRY”是最重要的单个因素。识别“重复”并将其删除是几乎所有其他编程构造的基石。换句话说,所有编程结构都至少部分存在,以帮助您创建DRY代码。首先说“从不复制”,然后练习识别和消除它。对于可能重复的内容要非常开放-即使它不是相似的代码也可能会重复功能...
Bill K,

11

我同意其他人的观点,即版本控制将立即解决您的许多问题。特别:

  • 无需维护已进行的更改的列表,或具有文件的大量副本等,因为这是版本控制所需要的。
  • 由于覆盖等原因,不再丢失任何文件(只要您只是坚持基本操作即可;例如,避免“重写历史记录”)
  • 无需过时的注释,无效的代码等,以防万一。一旦他们致力于版本控制,就可以随意批评他们。这可以感觉很解放!

我会说不要想太多:只需使用git。坚持使用简单的命令(例如,仅一个master分支),或者使用GUI,就可以了。作为奖励,您可以使用gitlab,github等免费发布和备份;)

我写这个答案的原因是要解决您可能尝试的两件事,而我上面没有提到。首先是使用断言作为单元测试的轻量级替代方案。单元测试通常位于功能/模块/被测试对象之外:它们通常将一些数据发送到功能中,接收返回的结果,然后检查该结果的某些属性。这通常是一个好主意,但由于以下几个原因可能会带来不便(尤其是“丢掉”的代码):

  • 单元测试需要确定将为功能提供哪些数据。该数据必须是现实的(否则几乎没有测试点),它必须具有正确的格式等。
  • 单元测试必须可以“访问”要声明的内容。特别是,单元测试不能检查函数内部的任何中间数据。我们将不得不将该功能分解为较小的部分,测试这些部分,然后将它们插入其他位置。
  • 单元测试也被认为与程序有关。例如,如果测试套件自上次运行以来发生了很大的变化,它们可能会变得“过时”,甚至可能有很多针对甚至不再使用的代码的测试。

断言没有这些缺点,因为在程序的正常执行过程中会对其进行检查。尤其是:

  • 由于它们是作为正常程序执行的一部分运行的,因此我们可以使用实际的真实数据。这不需要单独的策划,并且(根据定义)是现实的并且具有正确的格式。
  • 断言可以写在代码中的任何位置,因此我们可以将其放置在我们有权访问要检查的数据的任何位置。如果我们想测试一个函数的中间值,我们可以将一些断言放到该函数的中间!
  • 由于它们是内联编写的,因此断言不会与代码的结构“不同步”。如果我们确保默认情况下会检查断言,那么我们也不必担心它们会“过时”,因为我们将在下次运行程序时立即查看它们是否通过!

您提到速度是一个因素,在这种情况下,断言检查在该循环中可能是不可取的(但对于检查设置和后续处理仍然有用)。但是,几乎所有的断言实现都提供了一种将其关闭的方法。例如在Python中,显然可以通过运行该-O选项将其禁用(我不知道这一点,因为我之前从未感觉过需要禁用任何断言)。我建议您让他们继续默认; 如果您的编码/调试/测试周期变慢,则最好使用较小的数据子集进行测试,或者在测试过程中执行较少的模拟迭代,等等。如果由于性能原因而最终在非测试运行中禁用了断言,那么我建议您要做的第一件事就是测量它们是否实际上是导致速度下降的原因!(在性能瓶颈方面很容易自欺欺人)

我的最后建议是使用可管理依赖项的构建系统。我个人为此使用了Nix,但也听到了有关Guix的好消息。还有其他替代方案,例如Docker,从科学的角度来看,它们的用处远不如以前,但可能更熟悉。

像Nix这样的系统只是最近才变得流行(有些),有些人可能认为它们对于您所描述的“丢弃”代码是过大的,但是它们对于科学计算的可重复性的好处却是巨大的。考虑运行这样的shell脚本,例如run.sh

#!/usr/bin/env bash
set -e
make all
./analyse < ./dataset > output.csv

我们可以将其重写为Nix“派生”,例如run.nix

with import <nixpkgs> {};
runCommand "output.csv" {} ''
  cp -a ${./.} src
  cd src
  make all
  ./analyse < ./dataset > $out
''

两者之间的内容''...''是bash代码,与我们之前的代码相同,除了${...}可以用来“拼接”其他字符串的内容(在这种情况下./.,它将扩展到包含的目录的路径run.nix)。该with import ...行导入了Nix的标准库,该提供runCommand了运行中的bash代码。我们可以使用进行实验nix-build run.nix,它会给出类似的路径/nix/store/1wv437qdjg6j171gjanj5fvg5kxc828p-output.csv

那么,这能给我们带来什么呢?Nix将自动设置一个“干净”的环境,该环境只能访问我们明确要求的内容。特别是它无权访问变量$HOME或我们已安装的任何系统软件。这使结果独立于我们当前计算机的细节,例如~/.config我们碰巧已经安装的程序的内容或版本;也就是阻止他人复制我们的结果的东西!这就是为什么我添加了cp命令,因为默认情况下无法访问该项目。Nix脚本无法使用该系统的软件,这似乎很烦人,但是反之亦然:我们不需要在系统上安装任何东西(Nix除外)就可以在脚本中使用它。我们只需要它,Nix就会消失,并且需要获取/编译/进行任何必要的操作(大多数内容将作为二进制文件下载;标准库也很大!)。例如,如果我们想要一堆特定的Python和Haskell软件包,用于这些语言的某些特定版本,再加上一些其他的垃圾(因为为什么不这样?):

with import <nixpkgs> {};
runCommand "output.csv"
  {
    buildInputs = [
      gcc49 libjson zlib
      haskell.packages.ghc802.pandoc
      (python34.withPackages (pyPkgs: [
        pyPkgs.beautifulsoup4 pyPkgs.numpy pyPkgs.scipy
        pyPkgs.tensorflowWithoutCuda
      ]))
    ];
  }
  ''
    cp -a ${./.} src
    cd src
    make all
    ./analyse < ./dataset > $out
  ''

同样nix-build run.nix会运行此操作,获取我们首先请求的所有内容(并缓存所有内容,以备稍后使用)。输出(任何名为的文件/目录$out)将由Nix存储,这是它吐出的路径。它由我们要求的所有输入(脚本内容,其他程序包,名称,编译器标志等)的加密哈希标识;那些其他软件包则通过输入的哈希值来标识,依此类推,这样我们就可以对所有内容都拥有完整的服务链,可以追溯到编译了bash的GCC版本的GCC版本,等等!

希望我已经证明这为科学代码买了很多东西,并且相当容易上手。科学家也开始认真对待它,例如(Google热门歌曲)https://dl.acm.org/citation.cfm?id=2830172,因此可能是一种有价值的培养技能(就像编程一样)


2
非常详细且有用的答案-我真的很喜欢其他答案,但是断言听起来像是非常有用的第一步。
希瑟

9

无需全面了解版本控制+包装+单元测试的思维方式(这是您应该在某个时候尝试实现的良好编程习惯),我认为适合的一种中间解决方案是使用Jupiter Notebook。这似乎更好地与科学计算结合在一起。

它的优点是您可以将思想与代码混合在一起。解释为什么一种方法比另一种更好,并且在特设部分中保留了旧代码。除了正确使用单元格之外,自然会导致您将代码片段化并将其组织成有助于理解代码的功能。


1
此外,这确实有助于提高可重复性-例如,您可以运行完全相同的代码来生成发布图,或者返回到几个月前收起的内容,也许以纳入审阅者的评论。
afaulconbridge

对于想阅读更多内容的人,这也称为识字编程。
llrs

6

最佳答案已经很好,但是我想直接解决您的一些问题。

编写较小的代码是否需要单元测试?

代码的大小与单元测试的需求没有直接关系。它是间接相关的:单元测试在复杂的代码库中更有价值,而小型代码库通常不像大型代码库那么复杂。

对于容易出错的代码,或者当您将要实现此代码的许多实现时,单元测试就会大放异彩。单元测试对您当前的开发没有什么帮助,但是它们却可以防止将来发生会导致现有代码突然出现异常的错误(即使您没有碰到那件事)。

假设您有一个应用程序,其中库A执行数字平方,而库B应用勾股定理。显然,B依赖于A。您需要修复库A中的某些内容,并且说您引入了一个错误,该错误将数字平方而不是平方。

库B突然开始行为异常,可能会引发异常或仅给出错误的输出。当您查看库B的版本历史记录时,您会发现它没有被修改过。有问题的最终结果是,您没有任何迹象表明可能出了什么问题,并且您必须在意识到问题出在A上之前调试B的行为。这是浪费的精力。

输入单元测试。这些测试确认库A可以正常工作。如果您在库A中引入一个错误,导致其返回不良结果,那么您的单元测试将捕获该错误。因此,尝试调试库B不会卡住。
这超出了您的范围,但是在持续的集成开发中,只要有人提交一些代码,就会执行单元测试,这意味着您会尽快知道已经破坏了某些内容。

特别是对于复杂的数学运算,单元测试可能是一件幸事。您进行了一些示例计算,然后编写了单元测试,该单元测试将计算的输出与实际的输出进行了比较(基于相同的输入参数)。

但是,请注意,单元测试不会帮助您创建良好的代码,而是对其进行维护。如果您通常只编写一次代码而从未重访过它,那么单元测试的好处就会减少。

OOP怎么样?

OOP是一种思考不同实体的方法,例如:

当一个Customer想要购买的时候Product,他与谈话Vendor接收一个Order。然后Accountant将支付Vendor

将此与功能程序员对事物的看法进行比较:

当客户想要时purchaseProduct(),他会talktoVendor()这样sendOrder()对他。然后会计会的payVendor()

苹果和橘子。从客观上讲,它们都不比另一个更好。需要注意的一件有趣的事情是,对于OOP,它Vendor被提到两次,但它指的是同一件事。然而,函数式编程,talktoVendor()并且payVendor()是两回事。
这展示了方法之间的差异。如果这两个操作之间有很多共享的特定于供应商的逻辑,则OOP有助于减少代码重复。但是,如果两者之间没有共享的逻辑,则将它们合并为一个Vendor是徒劳的工作(因此,功能编程效率更高)。

通常,数学和科学计算是不依赖隐式共享逻辑/公式的独特运算。因此,功能编程比OOP更为常用。

相对于处理较大的项目,什么样的方法适合在进行“科学编程”时快速编写优质,清晰的代码?

您的问题意味着“好的,干净的代码”的定义会改变,无论您是在进行科学编程还是在进行较大的项目(我假设您的意思是企业级项目)。

好的代码的定义不会改变。但是,避免复杂性(可以通过编写简洁的代码来完成)的需求确实发生了变化。

同样的论点又回到了这里。

  • 如果您从不重访旧代码并完全理解逻辑而又无需将其划分开,那么请不要花费过多的精力来使事情可维护。
  • 如果您确实要重新访问旧代码,或者所需的逻辑过于复杂而无法一次解决所有问题(因此需要对解决方案进行划分),则应着重编写干净,可重用的结束代码。

我之所以问这些问题,是因为编程本身通常并不复杂。它更多地是关于我正在使用程序进行测试或研究的数学或科学。

我得到的区别是您在此处所做的区别,但是当您回顾现有代码时,就会同时看到数学和程序设计。如果无论是人为的还是复杂的,那么你就很难阅读。

例如,当两个变量和一个函数可能会照顾到它时,是否需要一个类?

除了OOP原则外,我编写类来容纳一些数据值的主要原因是因为它简化了方法参数的声明和返回值。例如,如果我有很多使用位置(纬度/经度对)的方法,那么我很快就会厌倦不得不键入内容,float latitude, float longitude并且会更喜欢编写Location loc

当您认为方法通常返回一个值(除非存在特定于语言的功能以返回更多值),而类似位置的东西希望您返回两个值(lat + lon)时,这会更加复杂。这激励您创建一个Location类来简化您的代码。

例如,当两个变量和一个函数可能会照顾到它时,是否需要一个类?

另一个需要注意的有趣事情是,您可以在不混合数据值和方法的情况下使用OOP。并非每个开发人员在这里都同意(有人称其为反模式),但是您可以拥有贫乏的数据模型,其中您具有单独的数据类(存储值字段)和逻辑类(存储方法)。
当然,这是在频谱上。您不需要完全贫血,您可以在认为适当时使用它。

例如,仅将一个人的名字和姓氏连接在一起的方法仍然可以放在Person类本身中,因为它不是真正的“逻辑”,而是一个计算值。

(考虑到一般情况下,程序的速度也应优先考虑更快的情况-当您运行25,000,000+个仿真时间步长时,您一定会希望它是这样。)

一个类总是与其字段总和一样大。Location再次以包含两个float值的示例为例,重要的是在此处注意,单个Location对象将占用两个单独的float值一样多的内存。

从这个意义上说,是否使用OOP都无关紧要。内存占用量是相同的。

性能本身也不是一个很大的障碍。例如,使用全局方法或类方法之间的区别与运行时性能无关,但与编译时生成字节码有关。

这样想:无论我用英语还是西班牙语写蛋糕食谱,都不会改变蛋糕需要30分钟烘烤的事实(=运行时性能)。食谱语言唯一改变的是厨师如何混合配料(=编译字节码)。

特别是对于Python,您不需要在调用之前显式地预编译代码。但是,当您不进行预编译时,将在尝试执行代码时进行编译。当我说“运行时”时,我指的是执行本身,而不是执行之前的编译。


6

干净的科学法规的好处

  • ...在看编程书籍时,它们似乎常常在大型项目中得到解决。

  • ...如果可以完成一些标准的工作,编写写为OOP的代码真的有意义,由于程序的简短,编写起来会快得多,并且具有相似的可读性吗?

从将来的编码器的角度考虑您的代码可能会有所帮助。

  • 他们为什么打开此文件?
  • 他们在找什么?

根据我的经验,

干净的代码应使验证结果变得容易

  • 使用户易于准确地知道他们需要做什么来运行您的程序。
  • 您可能需要对程序进行划分,以便可以分别对各个算法进行基准测试。

  • 避免编写带有反直觉副作用的函数,因为一个不相关的操作会导致另一操作的行为有所不同。如果无法避免,请记录您的代码需要什么以及如何设置它。

干净的代码可以用作将来的编码器的示例代码

清晰的注释(包括表明应如何调用函数的注释)和分隔良好的函数可以使刚开始(或将来对您)有用的东西对您的工作有用的时间有很大的不同。

除此之外,如果您决定将脚本制作成供其他人使用的真实库,那么为您的算法制作一个真实的“ API”可以使您更好地准备。

推荐建议

使用注释“引用”数学公式。

  • 在“引用”数学公式中添加注释,特别是如果您使用了优化(触发恒等式,泰勒级数等)时。
  • 如果您从书中获得了公式,请添加注释John Smith Method from Some Book 1st Ed. Section 1.2.3 Pg 180,说说,如果您在网站或论文中找到了该公式,也请引用。
  • 我建议避免使用“仅链接”注释,请确保您在某处通过名称引用该方法以允许人们使用它,我遇到了一些“仅链接”注释,这些注释已重定向到旧的内部页面,它们可能会令人沮丧
  • 如果仍然可以轻松地以Unicode / ASCII格式阅读,则可以尝试在注释中键入公式,但这可能会很尴尬(代码注释不是LaTeX)。

明智地使用评论

如果可以通过使用好的变量名/函数名来提高代码的可读性,请首先执行此操作。请记住,注释将永远存在,直到您将其删除为止,因此请尝试做出不会过时的注释。

使用描述性变量名

  • 如果单个字母变量是公式的一部分,则可能是最佳选择。
  • 对于将来的读者来说,能够查看您编写的代码并将其与正在实现的方程式进行比较可能至关重要。
  • 适当时,请考虑在其上添加后缀以描述其实际含义,例如。 xBar_AverageVelocity
  • 如前所述,我建议在某处的注释中明确指出您正在使用的公式/方法。

编写代码以对已知的良好和已知的不良数据运行程序。

编写较小的代码是否需要单元测试?

我认为单元测试可能会有所帮助,我认为科学代码的最佳单元测试形式是在已知的坏数据和好数据上运行的一系列测试。

编写一些代码来运行算法,并检查结果与预期的偏差程度。这将帮助您发现(可能非常糟糕并且很难找到)问题,在这些问题中您不小心对导致错误肯定结果的内容进行了硬编码,或者犯了使函数始终返回相同值的错误。

注意,这可以在任何抽象级别上完成。例如,您可以测试整个模式匹配算法,也可以测试在优化过程中仅计算两个结果之间距离的函数。首先从对您的结果最关键的区域开始,和/或您最关心的代码部分开始。

轻松添加新的测试用例,考虑添加“ helper”功能,并有效地构造输入数据。这可能意味着可能将输入数据保存到文件中,以便您可以轻松地重新运行测试,尽管要非常小心,以免出现误报或偏差/琐碎的测试用例。

考虑使用交叉验证之类的东西,有关更多信息,请参阅有关交叉验证的这篇文章

使用版本控制

我建议使用版本控制并将您的存储库托管在外部站点上。有些站点将免费托管资源库。

好处:

  1. 如果您的硬盘出现故障,它将提供备份
  2. 它提供了一个历史记录,使您不必担心最近出现的问题是否是由于您意外更改文件引起的,还有其他好处。
  3. 它允许您使用分支,这是处理长期/实验代码而不影响无关工作的好方法。

复制/粘贴代码时请谨慎

我的代码风格令人费解,并填充了过时的注释,这些注释指出了执行某项操作的替代方法或复制了几行代码。

  • 复制/粘贴代码可以节省您的时间,但这是您可以做的最危险的事情之一,尤其是如果代码不是您自己编写的(例如,如果是同事的代码)。

  • 一旦您的代码开始运行并经过测试,我建议您仔细检查一下它,以重命名任何变量或注释任何您不理解的内容。



6

通常发明该行业的工具来解决需求。如果需要使用该工具,如果不需要,则很可能不必使用。

具体而言,科学计划不是最终目标,而是手段。您编写该程序来解决您现在遇到的问题-您不希望该程序在十年内被其他人使用(并且必须维护)。这本身就意味着你不会想进入任何允许当前开发商以记录历史其他类似的版本控制,或捕捉功能的代码一样的单元测试工具。

那对您有什么好处?

  • 版本控制很不错,因为它使您可以非常轻松地备份您的工作。截至2018年,github是一个非常受欢迎的地方(您可以随时在需要时将其移动-git非常灵活)。备份的一种便宜而简单的替代方法是操作系统中的自动备份过程(适用于Mac的Time Machine,适用于Linux的rsync等)。您的代码需要在多个地方!
  • 单元测试非常好,因为如果您首先编写它们,则必须考虑如何检查代码的实际作用,这有助于您为代码设计更有用的API。如果您打算编写代码以便以后重用,并且在更改算法时会有所帮助,这将很有帮助,因为您知道它可以在这些情况下使用。
  • 文档。学习用您使用的编程语言编写适当的文档(例如Javadoc for Java)。为未来写信。在此过程中,您会发现良好的变量名使记录变得容易。重复。与诗人一样,对您的文档给予同等的照顾。
  • 使用好的工具。查找可以帮助您并很好地学习它的IDE 。像将变量重命名为更好的名称那样进行重构,这种方式要容易得多。
  • 如果您有同行,请考虑使用同行评审。让局外人查看并理解您的代码是您编写的未来的即刻版本。如果您的同龄人不理解您的代码,那么您以后也可能不会。

这个答案怎么没有收到赞?现在。我们的小组发现同行评审是所有工具中最有效的工具之一,在涉及科学代码时,它比单元测试重要得多。将科学期刊文章中的一组复杂方程式转换为代码时,很容易出错。科学家和工程师常常造就极其贫穷的程序员。同行评审会发现架构上的丑陋之处,使代码难以维护/理解/使用。
大卫·哈门

5

除了这里已经提供的好的建议之外,您可能还需要考虑编程的目的,因此对您来说重要的是什么。

“更多的是关于我正在用程序进行测试或研究的数学或科学。”

如果目的是为了自己理解而进行实验和测试,并且知道结果应该是什么,那么您的代码基本上可以快速投入使用,尽管可以改进,但是您当前的方法可能就足够了。如果结果与预期不符,您可以返回并查看。

但是,如果编码结果指示了研究方向,而您不知道结果应该是什么,那么正确性就变得尤为重要。代码中的错误可能导致您从实验中得出错误的结论,对您的整体研究产生各种不利影响。

在这种情况下,使用单元测试将代码分解为易于理解和验证的功能将为您提供更坚实的构建基石,使您对结果有更大的信心,并可能避免以后遇到很多麻烦。


5

与版本控制和单元测试一样,它可以使您的整体代码井井有条,功能正常,但实际上并不能帮助您编写更简洁的代码。

  • 版本控制将使您看到代码如何以及何时变得混乱。
  • 单元测试将确保即使代码完全混乱,它仍然可以正常工作。

如果要阻止自己编写混乱的代码,则需要一个在混乱发生的地方起作用的工具:编写代码时。一种流行的工具称为棉短绒。我不是python开发人员,但看起来Pylint可能是一个不错的选择。

短毛猫会查看您编写的代码,并将其与一组可配置的最佳实践进行比较。如果linter有一个变量必须为的规则camelCase,并且您在中写了一个snake_case,它将标记为错误。优良的短子有从“必须使用声明的变量”到“函数的圈复杂度必须小于3”的规则。

可以将大多数代码编辑器配置为在每次保存时或通常在键入时运行linter,并内联指出问题。如果您输入x = 7,则x将会高亮显示,并带有使用更长更好的名称的说明(如果这是您配置的名称)。这就像大多数文字处理程序中的拼写检查一样,使其难以忽略,并有助于养成更好的习惯。


这应该有更多的支持。+1
希瑟

2
但是,为了天堂的缘故,请确保您知道如何将短绒配置成您喜欢的样式,否则它将使您大为恼火。
DrMcCleod '18年

4

您列出的所有内容都是隐喻工具箱中的工具。就像生活中的任何事物一样,不同的工具适用于不同的任务。

与其他工程领域相比,软件可以单独处理很多独立的部分。分配声明不会根据房间的温度波动而做出不同的评估。一种if说法不腐蚀到位,并保持一段时间后返回同样的事情。但是由于单个元素是如此简单,并且是由人类编写的软件,所以这些元素被组合成越来越大的部分,直到结果变得如此庞大和复杂,从而达到人们可以进行精神管理的极限。

随着软件项目的发展,人们已经对其进行研究并创建了工具来尝试管理这种复杂性。OOP就是一个例子。越来越多的抽象编程语言是另一种方法。由于软件中的大量资金正在做更多的事情,因此您将看到和了解实现这些目标的工具。但似乎这些情况不适用于您。

因此,不要觉得您需要做任何事情。归根结底,代码只是达到目的的一种手段。不幸的是,最好的方法是在一些较大的项目上为您提供正确的观点和不正确的观点,因为当您想到工具箱时,很难知道缺少了什么。

无论如何,只要您的脚本很小,我都不会担心不使用OOP或其他技术。您描述的许多问题只是一般的专业组织技能,即,不丢失旧文件是所有字段都必须处理的事情。


4

除了到目前为止提供的所有好的建议之外,我随着时间的推移学会了一种发现并且很重要的实践,那就是非常自由地在代码中添加详细的注释。对我来说,当我经过很长一段时间后又回到某件事上时,这是最重要的一件事。向自己解释自己的想法。这需要花费一些时间,但是相对来说比较容易,而且几乎没有痛苦。

有时我的注释行是代码的两倍或三倍,尤其是当这些概念或技术对我来说是新事物并且需要向我自己解释时。

完成以上所有操作,进行版本控制,改进实践等。但是,随身携带一些东西给自己解释。它真的很好。


4

对于这种程序,什么素质很重要?

它是否易于维护或发展可能无关紧要,因为这种可能性不会发生。

它的效率如何可能并不重要。

它是否具有出色的用户界面或是否可以抵御恶意攻击者,可能并不重要。

它具有可读性可能很重要:阅读您的代码的人可以轻松地使自己相信它已经按照要求执行了操作。

正确是很重要的。如果程序给出的结果不正确,那就是您窗外的科学结论。但是它只需要正确处理您实际要求处理的输入即可;如果给定负的输入数据值,如果所有数据值均为正,则是否落差并不重要。

保持一定水平的变更控制也很重要。您的科学结果必须具有可重复性,这意味着您需要知道程序的哪个版本产生了要发布的结果。因为只有一个开发人员,所以变更控制不需要非常复杂,但是您需要确保可以回到某个时间点并重现结果。

因此,不必担心编程范例,面向对象,算法优雅。不必担心其清晰度和可读性以及随时间推移所做更改的可追溯性。不用担心用户界面。不必担心测试输入参数的所有可能组合,但要进行足够的测试以确信(并使其他人确信)您的结果和结论有效。


4

我曾在与类似的环境中一起工作过,他们编写了很多(数学/科学)代码,但是由于您所描述的相同原因,他们的进度很慢。但是,我注意到一件事情进展顺利,我认为这也可以对您有所帮助:建立并维护一组专门的库,这些库可以在多个项目中使用。这些库应提供实用程序功能,因此将有助于使您当前的项目保持特定于问题域。

例如,您可能必须在自己的领域中处理许多坐标转换(ECEF,NED,纬度/经度,WGS84等),因此类似的函数convert_ecef_to_ned()应该进入名为的新项目中CoordinateTransformations。将项目置于版本控制下,并将其托管在部门的服务器上,以便其他人可以使用(并希望对其进行改进)。然后,几年后,您应该拥有一个功能强大的库集合,并且您的项目仅包含特定于特定问题/研究领域的代码。

一些更一般的建议:

  • 无论是什么问题,始终要尽可能准确地建模您的特定问题。这样,软件设计问题(例如,在何处/在何处/如何放置变量)应该变得更加显而易见。
  • 我不会打扰测试驱动的开发,因为科学代码描述了想法和概念,并且更具创造力和流动性。无需定义API,无需维护服务,更改功能时他人代码的风险等。

不要让其他人改进它。可能是他们不了解代码的目的,只会把事情搞砸了。
mathreadler '18

@mathreadler好吧,如果它们是通用实用程序库,那么其他人很难搞清楚,这就是想法。
jigglypuff

为什么很难弄乱通用库?如果您不知道自己在做什么,或者如果您真的很努力,那么事情就不那么困难了。
mathreadler '18

@mathreadler因为例如通常只有一种方法可以进行坐标转换或单位转换。
jigglypuff

通常有很多方法,具体取决于数字在内存中的存储方式,它们使用的表示形式以及许多其他内容,打算为该库编译的CPU。例如,一个编码器可能假定每个人都将始终使用IEEE双精度,但是另一种编码器几乎总是使用单精度或其他三分之一奇怪的格式。然后,一个编码器将使用模板多态性,但是另一位编码器可能对此过敏,大约三分之一甚至更怪异的一个将对低级c或汇编语言中的所有内容进行硬编码。
mathreadler '18

3

以下是我的观点,并且受到我自己的特定路径的很大影响。

编码通常会使您对应该如何做的事情产生教条式的看法。我认为您需要查看累计价值和成本来决定适当的策略,而不是技巧和工具。

编写良好的,可读的,可调试的可靠代码需要花费大量时间和精力。在许多情况下,由于规划范围有限,因此不值得这样做(分析瘫痪)。

一个同事有一个经验法则。如果您第三次要做基本上相同的事情,那么请投入精力,否则,快速而肮脏的工作是适当的。

进行某种测试是必不可少的,但是对于一个不在项目中的项目,只需进行目测就足够了。对于任何实质性的事情,测试和测试基础结构都是必不可少的。价值在于它使您在编码时解放了,代价是如果测试着重于特定的实现,那么测试也需要维护。测试还会使您想起事物是如何运作的。

对于我自己的一个脚本(通常用于验证概率估计或类似的东西),我发现两个非常有用的小东西:1.包含一个注释,说明如何使用代码。2.简要说明您编写代码的原因。当您编写代码时,这些事情非常明显,但是明显性却浪费时间:-)。

OOP是关于重用代码,抽象,封装,分解等的。非常有用,但是如果生成高质量的代码和设计不是您的最终目标,那么很容易迷失方向。生产高质量的东西需要时间和精力。


3

尽管我认为单元测试有其优点,但它们对科学发展却没有可疑的价值-它们通常太小而无法提供很多价值。

但是我真的很喜欢科学代码的集成测试:

隔离一小段可以自己运行的代码,例如ETL管道。然后编写一个提供数据的测试,运行etl管道(或仅执行一个步骤),然后测试结果是否符合您的期望。尽管测试的块可以包含很多代码,但测试仍提供了价值:

  1. 您有一个方便的钩子可以重新执行代码,这有助于经常运行它。
  2. 您可以在测试中测试一些假设
  3. 如果某些想法破了,很容易添加失败的测试并进行修复
  4. 您可以对预期的输入/输出进行编码,从而避免了通常因猜测输入数据格式而引起的麻烦。
  5. 尽管不如单元测试那么精简,但IT测试仍然有助于将您的代码分开,并迫使您在代码中添加一些界限。

我经常使用这种技术,并且通常以相对易读的主要功能结尾,但是子功能通常又长又丑,但由于强大的I / O边界,可以快速修改和重新排列。


2

我通常在非常庞大的源代码基础上工作。我们使用您提到的所有工具。最近,我开始为副项目开发一些python脚本。它们最多只有几十到几百行。出于习惯,我将脚本提交给了源代码管理。这很有用,因为我可以创建分支来尝试可能不起作用的实验。如果需要复制代码并出于其他目的对其进行修改,我可以进行分叉。这样可以保留原件,以防万一我需要再次拿出来。

对于“单元测试”,我只有一些输入文件,这些文件旨在产生一些手工检查的已知输出。我可能可以使它自动化,但感觉这样做要比花时间节省更多时间。这可能取决于我必须多久修改和运行一次脚本。无论哪种方式,如果可行,都可以做到。如果麻烦多于其价值,请不要浪费时间。


2

使用编写代码(与编写一般代码一样),主要问题是:

您要考虑哪个读者?或谁在使用您的代码?

当您是唯一的受众时,像正式的编码准则之类的事情就毫无意义。

话虽这么说,以某种方式编写代码将很有帮助,您将来可以立即理解它。

因此,“好风格”将是其中一种,对您有最大的帮助。这种风格应该是我无法给出的答案。

我认为对于150 LOC的文件,您不需要OOP或单元测试。当您有一些不断发展的代码时,专用的VCS将很有趣。否则,.bak就可以了。这些工具可以治愈疾病,您甚至可能没有。

也许您应该以这种方式编写代码,即使您在醉酒时阅读它,也能够阅读,理解和修改它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.