1.4 如何成为高性能程序员
长期看,要成功地确保项目的高性能,编写出高性能代码只是一个方面。相比于加速和复杂的解决方案,整个团队的效率要重要得多。为确保团队效率,有几个因素至关重要:良好的结构和文档、易于调试以及遵循相同的标准。
假设你开发了一个原型,它虽未经详尽测试,也没有经过团队审核,但看起来“足够好”,因此被推送到了生产环境中。因为它从来没有以结构化的方式编写,所以它缺乏测试且没有文档化。突然之间,这增添了需要他人来支持的代码,而管理人员通常无法评估团队将为此付出什么样的代价。
这种解决方案难以维护,往往不受欢迎(没人去修改其结构,没人去添加可帮助重构的测试,更没有其他人愿意接手),因此确保其正常运行的工作始终落在最初接手的那个开发人员身上。这可能在紧急的情况下导致可怕的瓶颈,还可能带来重大风险:如果这位开发人员离开了,结果将如何呢?
通常,管理团队对难以维护的代码带来的惯性认识不足时,就会出现这种开发风格。事实证明,从长远看,测试和文档有助于保持团队的高效率,还有助于说服管理层分配用于整理原型代码的时间。
在研究环境中验证各种想法和数据集时,经常会在未遵循良好编码实践的情况下创建大量Jupyter Notebook。这背后的理念是以后再正确地编写,但实际上根本没有以后。因此最终的成果是一系列可运行的代码,但没有对代码进行重现、测试和确保它们可信任的基础设施。同样,这种成果带来的风险很高,而可信度又很低。
为避免上述情况发生,可采用以下通用方法。
● 确保可行:先创建一个足够好的解决方案。创建用后即弃的原型解决方案合乎情理,这让你能够在第二个版本中采用更佳的结构。编码前做些规划工作总是明智的选择,不然你肯定会后悔“整个下午都在编码,而没有花1小时来思考”。在有些领域,这被称为“三思而后行”。
● 确保正确:接下来,添加强大的测试套件,并辅以良好的文档和清晰的重现说明,让其他团队成员能够快速接手项目。
● 提高速度:最后,将重点放在剖析和编译或并行化上,使用既有测试套件核实改进后的解决方案仍然可以按预期工作。
1.4.1 最佳实践
有一些东西是必不可少的,这包括文档、良好的结构和测试,它们至关重要。
项目级文档有助于确保结构始终清晰,还可在未来给你和同事提供帮助;如果你省略这部分,没有人(包括你自己)会感谢你。一种合理的做法是,先将这种文档作为一个顶级README文件,以后必要时再将其扩展为一个docs/文件夹。
阐述项目的目的、各个文件夹包含的内容、数据来自何方、哪些文件至关重要以及如何运行项目(包括测试)。
Micha推荐同时使用Docker,这样将有一个顶级Dockerfile,它准确地指出了要成功地运行项目,需要哪些操作系统库,还让你能够轻松地在其他计算机中运行项目以及将其部署到云环境中。
添加一个包含单元测试的tests/文件夹。我们喜欢使用测试框架pytest,因为它是建立在Python内置模块unittest的基础之上的。先编写两三个测试,然后慢慢添加,再逐步采用覆盖工具,因为它会指出测试覆盖了多少行代码,有助于避免令人讨厌的意外情况。
如果继承了缺乏测试的遗留代码,那么先给它添加测试将带来极高的回报。另外,编写一些集成测试,对整个项目流程进行检查,确认特定的数据输入将生成特定输出,这有助于你在以后修改代码时保持理智。
每当遇到代码出现问题时,都添加一个测试。在同一个地方跌倒两次毫无意义。
在代码中,给每个函数、类和模块都编写文档字符串,这大有裨益。你的目标是提供有用的描述,指出函数实现了什么,并在可能的情况下通过简短的示例指出函数的预期输出。要获得灵感,请看看numpy和scikit-learn中的文档字符串。
每当你发现代码块太长(如函数超过一屏)时,一定要通过重构来缩短它们。代码块越短,测试和支持起来就越轻松。
提示:编写测试时,请考虑遵循测试驱动的开发方法。在准确地知道需要开发什么且有现成的可测试示例的情况下,这种方法的效率极高。
你编写并运行测试,发现它们不能通过,再添加函数和最起码的逻辑,让测试得以通过。对所有的工作都进行测试后,就大功告成了。预先确定函数期望的输入和输出后,你将发现函数的逻辑实现起来非常容易。
如果你无法预先定义测试,那么一个自然而然的问题是,你真的明白相应的函数需要做什么吗?如果不明白,又怎么可能正确而高效地编写它呢?如果你采用的流程是创造性的,且对要研究的数据认识不太深刻,那么这种方法就不管用了。
务必进行源代码版本控制,当你在不方便的时候需要重写某些代码时,一定会为自己进行版本控制感到庆幸。养成频繁提交(每天乃至每10分钟提交一次)以及每天推送到仓库的习惯。
务必遵循编码标准PEP8。锦上添花的做法是,对未提交的版本控制钩子采用black(非常严格的代码格式设置程序),使其根据标准修改代码。同时,使用flake8对代码进行检查,以避免其他错误。
创建与操作系统隔离的环境可让工作更轻松。作者Ian喜欢使用Anaconda,而Micha 喜欢与Docker配套的pipenv。这两种方案都可行,比使用操作系统的全局Python环境要好得多。
别忘了自动化是你的朋友,减少手动工作就意味着降低了错误出现的概率。通过使用自动化的测试套件运行程序,将构建系统、持续集成的工作自动化,而通过自动化系统部署过程,可将易于出错的烦琐任务变成任何人都能够运行和支持的标准流程。
最后,别忘了可读性远比抖机灵重要。在你和同事的维护工作中,简短但复杂而难以理解的代码片段将是拦路虎,让人感到害怕,不敢去触碰。因此,请让函数更容易理解(哪怕这样导致其代码更长),并辅以有用的文档,指出函数将返回什么,同时添加测试,用以核实函数像你期望的那样工作。
1.4.2 对Notebook最佳实践的思考
Jupyter Notebook虽然非常适合用于以视觉化方式交流,但会惯出用户懒惰的毛病。如果你发现Notebook中有很长的函数,请务必将它们提取到Python模块中,再添加相应的测试。
可考虑在IPython或QTConsole中编写原型代码,再将代码行转换为Notebook中的函数,然后将函数提取到模块中并添加配套的测试。最后,如果封装和数据隐藏会有所帮助,请考虑将代码封装在类中。
为检查函数是否像你期望的那样工作,Notebook中可能充斥着assert语句。在Notebook中,除非对函数进行重构,将其放到模块中,否则无法轻松地测试代码,而assert检查是一种实现进一步验证的简单方式。除非将代码提取到模块中,并编写合理的单元测试,否则它们是不可信的。
不提倡在代码中使用assert语句来检查数据。它是一种核实特定条件是否满足的简单方式,但并不符合Python的语言习惯。为让代码更容易理解,请检查数据的状态是否符合预期,并在不符合预期时引发合适的异常。一种常见的异常是ValueError,它在函数收到的值不符合预期时触发。Bulwark库是一个专注于Pandas的测试框架,可用于检查数据是否满足指定的约束条件。
你可能还需在Notebook末尾添加一些完整性检查,这包括逻辑检查以及指出结果符合预期的raise和print语句。当你半年后再来看这些代码时,肯定会庆幸你以前所做的工作,让你能够轻松地确定它们是否能正确地工作。
使用Jupyter Notebook的一个麻烦是,难以将代码与版本控制系统共享。nbdime是一套新工具,让你能够比较不同的Notebook。这是一款救命神器,让你能够与同事协同工作。
1.4.3 重新发现工作的乐趣
人生可能复杂难懂。从本书第1版推出到现在已过去了5年,在此期间,两位作者的家人和朋友遭遇了很多变故,其中包括抑郁、癌症、搬家、生意上的成功和失败以及职业方向的转变。这些外部变故不可避免地会影响工作和生活前景。
千万别忘了不断地寻找工作和生活中的乐趣。只要开始寻找,总能发现有趣的细节或要求。你可能会问,他们为何会做出那样的决定,如果换成我,会做出什么不同的决定。这可能让你醍醐灌顶,顺利地开启有关如何改变或改进的对话。
将值得庆贺的事情记录下来。人们很容易深陷日常琐事,将成绩抛诸脑后;当你为跟上时代不断向前奔跑时,就会忘记自己取得的巨大进步。
建议你用一个清单将值得庆贺的事项记录下来,并注明是如何庆贺的。Ian就有一个这样的清单,每当他去更新这个清单时都会又惊又喜:原来上一年竟然有如此多的好事,要不是更新清单,他早已将这些好事忘在脑后了。这个清单不仅包含工作成就,还有业余爱好和运动方面的好事,以及庆贺成就的情况。Micha的做法是,确保将个人生活放在第一位,并在远离计算机的时候专注于非技术项目。不断提高技能至关重要,但这并不意味着必然失去热情!
编程有赖于好奇心,还有对深究技术细节的乐此不疲,在需要专注于性能时尤其如此。可惜当你失去热情后,首先消失的就是这种好奇心,因此请花时间确保旅途愉快,将乐趣和好奇心保留下来。