编码过程与规范
软件编程是一个复杂而迭代的过程,它不仅仅是编写代码,还应该包括代码审查、单元测试、代码优化、集成调试等一系列工作,并且这些工作不是一蹴而就的,而是不断迭代、循环完成的:
按照 高质量软件开发之道 ,我们在本节探讨规范编码的问题。
软件编程规范
软件编码规范是与特定语言相关的描写如何编写代码的规则集合。
实际工程中,软件全生命周期的 70% 成本是维护,并且在其生命周期中很少由原编写人员进行维护,因此为了提高编码质量、避免不必要的程序错误、增强程序代码的可读性、可重用性和可移植性,我们必须遵循特定的编程规范。
不同的企业/项目都有各自的编程规范这里我们参考 Google 的编程规范:Python Style Guide
Python 编程规范
程序模板
- 在文件中包含中文字符时,需要标注好正确的编码格式;
- 导入模块时应遵循这样的顺序:Python 自带模块、第三方模块、个人项目的模块
if __name__ == '__main__
语句的作用:检查文件是否是直接执行。因为 pydoc 、单元测试等场景中,都要求每一个文件都是可导入的,而且考虑到代码的最大可重用性,即使部分文件是打算直接执行的,但未来也有被其它文件作为模块导入的可能。加上对 name 的判断后,主程序只有在被直接执行时才会执行,其他情况下则作为模块导入。
注释
Python 中有两种注释:
- 第二种以文档字符串编写的注释,可以通过 pydoc 等工具自动化地生成对应文档:
编写注释要恰如其分,而不是面面俱到:
- 这样重复代码语句的含义对帮助理解代码没有任何作用:
- 实际上在代码中只需要解释关键部分的作用和实现原理即可,具体的实现过程通过阅读良好命名和规范的代码语句应当能够理解:
- 要记住这些编写注释的规则:
- 好的注释解释为什么,而不是怎么样
- 不要在注释中重复描述代码
- 当自己在编写密密麻麻的注释来解释代码时,需要停下来看是否存在更大的问题
- 想一想在注释中写什么,不要不动脑筋就输入
- 写完注释之后要在代码的上下文中回顾一下,它们是否包含正确的信息?
- 当修改代码时,维护代码周围的所有注释
命名
- 下划线式:
module_name
,package_name
,method_name
,function_name
,global_var_name
,instance_var_name
,function_parameter_name
,local_var_name
,query_proper_noun_for_thing
,send_acronym_via_https
,- 大写式:
GLOBAL_CONSTANT_NAME
,- 驼峰式:
ClassName
,ExceptionName
,
好的名字应当一目了然,不需要读者去猜,甚至不需要注释:
- Python 库的命名约定有点混乱,因此很难使之变得完全一致,不过还是有公认的命名规范
- 新的模块和包(包括第三方的框架)必须符合这些标准,但对已有的库存在不同风格的,保持内部的一致性是首选的
- 因此对于老的模块、第三方模块中,不符合命名规范的部分,可以看作特例,但我们自己编写项目时,应当遵循这样的规范
为什么要这样劳力地遵循命名规范?不是有文档、注释吗?
- 这是因为需要外部文档支持的代码是脆弱的,要确保你的代码本身读起来就很清晰
- 这就要求编写自文档化的代码:唯一能完整并正确地描述代码的文档是代码本身;代码本身的可读性就很高
例如对左边这样非常抽象的代码,应当重构成右边命名规范、可读性良好的代码:
语句
3.14 Statements
Generally only one statement per line.
However, you may put the result of a test on the same line as the test only if the entire statement fits on one line. In particular, you can never do so with
try
/except
since thetry
andexcept
can’t both fit on the same line, and you can only do so with anif
if there is noelse
.只有在特定情况下,将测试结果与测试放在同一行才不会出错。
特别是,在使用
try
/except
时绝对不能这样做,因为try
和except
不能放在同一行,只有在没有else
的情况下才能使用if
。Yes: if foo: bar(foo)
Link to originalNo: if foo: bar(foo) else: baz(foo) try: bar(foo) except ValueError: baz(foo) try: bar(foo) except ValueError: baz(foo)
缩进
使用 4 个空格作为缩进,而非 Tab ,并且不要混用 tab 与空格。
导入模块
- import 次序:先 import Python 内置模块,再 import 第三方模块,最后 import 自己开发的项目中的其它模块;这几种模块中用空行分隔开来。
- 一条 import 语句 import 一个模块。
- 当从模块中 import 多个对象且超过一行时,使用如下断行法(py2.5 以上版本):
from module import (obj1,obj2,obj3,obj4,obj5,obj6)
- 不要使用
from module import *
,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。
语法糖
慎用 Python 高版本的语法糖:
- 这些特性有时并不稳定,也不是所有项目参与者都会掌握;
良好的编程实践
想要成为优秀的开发者,离不开这三步:
- 看:阅读优秀的代码,学习别人的代码
- 问:如何向开源社区提问题
- 练:亲自动手编写代码,实践、实践、再实践
软件开发时,先要分而治之地理解复杂的问题,再反之将分治的小问题逐个解决,最后合起来做成一整套的解决方案:
按照 高质量软件开发之道 ,我们在本节探讨高质量设计的问题。
模块化设计
模块化设计古已有之,活字印刷术就是模块化设计的一个典型范例,其中每个汉字代表一个文字“模块”,具有特定的功能和含义,而语法是连接汉字模块的“接口”,通过它构成了最终的文章“产品”。
模块化程序设计的基本思想就是,将一个大的程序按功能分拆成一系列小模块。这样做的好处有许多:
- 降低程序设计的复杂性
- 提高模块的可靠性和复用性
- 缩短产品的开发周期
- 易于维护和功能扩展
从不同的角度进行模块划分,可以获得不同的模块:
- 并且对于软件中模块的变动特点,应当予以区分,将易变的逻辑与稳定的功能模组独立开:
- 另外,模块应当是单一职责的,即类或者函数应该只做一件事,并且做好这件事。不过单一职责并不等价于单一功能,这里的“职责”更准确的含义是引起变化的原因——如果一个大模块中有很多因素可以改变,那么就应当对其进行拆分;
面向抽象编程
在模块化设计的基础上,我们可以先设计出各个模块的“骨架”,或者说对各个 模块进行“抽象”,定义它们之间的“接口”。定义各个模块互相关联的部分,这些部分在未来开发中不应该轻易发生改变。
注:这里所说“接口”与 Java 的 interface 类似,但不一定需要显式地定义出来,也可以是开发 人员之间的约定。
错误与异常处理
错误是导致程序崩溃的问题,例如 Python 程序的语法错误(解析错误)或者未捕获的异常(运行错误)等。
异常是运行时期检测到的错误,即使一条语句或者表达式在语法上是正确的,当试图执行它时也可能会引发错误。
异常处理是用于管理程序运行期间错误的一种方法,这种机制将程序中的正常处理代码和异常处理代码显式地区别开来,提高了程序的可读性。
Python 中进行异常处理,需要使用 try/except
块进行包围,但并非盲目地将一大堆代码都塞进去,那样势必会导致抛出的异常要么类型范围过大(比如本是文件 IO 时出错的 IOError
,却抛出一个模糊的 Error
),因此我们应该这样做:
- 减少
try/except
块中的代码量,细化具体的异常类型,更有针对性地处理; - 在关键部分应该检查变量的合法性,包括类型和取值范围等,以避免“雪球效应”,扼之于摇篮中:
案例:生命游戏
生命游戏的规则是从初始状态开始,每过一个时间单位,细胞都判断一次周围的细胞存活情况:
- 如果周围有 4 个活细胞,则当前细胞因竞争不到营养而死;若有 3 个活细胞,则可以存活;若有 2 个活细胞,则保持当前状态不变;若只有 1 个活细胞,则会因孤独而死;
那么我们应当如何实现这个游戏?从模块化的角度来看,我们应当如何分解?
- 模块化设计可以有多种不同的方案,应该选择利于理清思路、方便测试、容易调整的方案,同时避免“过度设计”;
- 在模块化分解之后,开发人员可以分别实现各个模块。根据函数单一职责的原则,各个模块内部还会定义更多的函数。与此同时,模块测试的设计工作也可以开始进行。
谨慎修改关键函数
前面抽象得到的各模块关键函数在后续开发中不应发生改变,这些函数一旦参数列表/名称/返回值等发生变化,可能造成连带性的一系列修改。
在确定好模块后,我们应当设计好连接这些模块的接口,通过统一的接口进行相互调用:
我们的生命游戏是自初始化后就无休止地运行下去的,要停止,则应在命令行中使用 Ctrl+C 的方式终止,但这会引发 Python 中的 KeyboardInterrupt
异常,因此我们可以对之进行捕获:
代码静态检查
代码审查
代码审查(Code Review)是一种用来确认方案设计和代码实现的质量保证机制,它通过阅读代码来检查源代码与编码规范的符合性以及代码的质量。
代码审查有如下好处:
- 可以检查设计的合理性
- 互为 Backup ,通过多重备份可以保证项目开发的稳定、健壮
- 作者和审查者之间可以分享知识、设计、技术
- 可以增加代码可读性、处理代码中的“地雷区”
可以阅读这个网站更深一步地了解 code review :Code Review Best Practices 。
代码审查项目
-
编程规范:
- 按照具体编程语言的编码规范进行检查,包括命名规则、程序注释、 缩进排版、声明与初始化、语句格式等
-
面向对象设计:
- 类的设计和抽象是否合适
- 是否符合面向接口编程的思想
- 是否使用合适的设计模式
-
性能方面:
- 在出现海量数据时,队列、表、文件在传输、上载等方面是否会出现问题,是否控制如分配的内存块大小、队列长度等
- 对 Hashtable、Vector 等集合类数据结构的选择和设置是否合适
- 有无滥用 String 对象的现象
- 是否采用通用的线程池、对象池等高速缓存技术以提高性能
- 类的接口是否定义良好,如参数类型等应避免内部转换
- 是否采用内存或硬盘缓冲机制以提高效率?
- 并发访问时的应对策略
- I/O 方面是否使用了合适的类或采用良好的方法以提高性能(如减 少序列化、使用 buffer 类封装流等)
- 同步方法的使用是否得当,是否过度使用?
- 递归方法中的迭代次数是否合适(应保证在合理的栈空间范围内)
- 如果调用了阻塞方法,是否考虑了保证性能的措施
- 避免过度优化,对性能要求高的代码是否使用 profile 工具
-
资源释放处理:
- 分配的内存是否释放,尤其在错误处理路径上(如 C/C++)
- 错误发生时是否所有对象被释放,如数据库连接、Socket、文件等
- 是否同一个对象被释放多次(如 C/C++)
- 代码是否保存准确的对象引用计数
-
程序流程:
- 循环结束条件是否准确
- 是否避免了死循环的产生
- 对循环的处理是否合适,应考虑到性能方面的影响
-
线程安全:
- 代码中所有的全局变量是否是线程安全的
- 需要被多个线程访问的对象是否线程安全,检查有无通过同步方法保护
- 同步对象上的锁是否按相同的顺序获得和释放以避免死锁,注意错误处理代码
- 是否存在可能的死锁或是竞争,当用到多个锁时,避免出现类似情况:线程 A 获得锁 1,然后锁 2,线程 B 获得锁 2,然后锁 1
- 在保证线程安全的同时,注意避免过度使用同步,导致性能降低
-
数据库处理:
- 数据库设计或 SQL 语句是否便于移植(注意与性能会存在冲突)
- 数据库资源是否正常关闭和释放
- 数据库访问模块是否正确封装,便于管理和提高性能
- 是否采用合适的事务隔离级别
- 是否采用存储过程以提高性能
- 是否采用 PreparedStatement 以提高性能
-
通讯方面:
- Socket 通讯是否存在长期阻塞问题
- 发送接收的数据流是否采用缓冲机制
- Socket 超时处理和异常处理
- 数据传输的流量控制问题
-
JAVA 对象处理:
- 对象生命周期的处理,是否对象引用已失效可设置 null 并被回收
- 在对象传值和传参方面有无问题,对象的 clone 方法使用是否过度
- 是否大量经常地创建临时对象
- 是否尽量使用局部对象(堆栈对象)
- 在只需要对象引用的地方是否创建了新的对象实例
-
异常处理:
- 每次当方法返回时是否正确处理了异常,如最简单的处理是记录日 志到日志文件中
- 是否对数据的值和范围是否合法进行校验,包括使用断言
- 在出错路径上是否所有的资源和内存都已经释放
- 所有抛出的异常是否都得到正确的处理,特别是对子方法抛出的异 常,在整个调用栈中必须能够被捕捉并处理
- 当调用导致错误发生时,方法的调用者应该得到一个通知
- 不要忘了对错误处理部分的代码进行测试,很多代码在正常情况下 执行良好,而一旦出错整个系统就崩溃了?
-
方法(函数):
- 方法的参数是否都做了校验
- 数组类结构是否做了边界校验
- 变量在使用前是否做了初始化
- 返回堆对象的引用,不要返回栈对象的引用
- 方法的 API 是否被良好定义,即是否尽量面向接口编程,以便于维护和重构
-
安全方面:
- 对命令行执行的代码,需要详细检查命令行参数
- WEB 类程序检查是否对访问参数进行合法性验证
- 重要信息的保存是否选用合适的加密算法
- 通讯时考虑是否选用安全的通讯方式
-
其他:
- 日志是否正常输出和控制
- 配置信息如何获得,是否有硬编码
Pylint
详细使用方法可以参考:Pylint-Get-Started 。
以下是其中一个使用 pylint 进行代码分析的实例:
Instance
以如下代码为例使用 pylint 进行分析:
分析结果及报告如下:
对其中内容我们详细讲解:
首先是 pylint 对错误列表进行分析:
- 图片中错误列表的每一行从左到右的内容分别是错误类型、出现位置、说明信息:
- 我们的输出部分与图片中不太一样,这是因为版本比较新的缘故,其中给出了错误代号,比如
invalid-name
的错误代号是C0103
,具体代号对应什么错误,可以参考相关 wiki :PyLint Messages- 这里 C 类型的报错是指其与
pylintrc
文件中制定的编码标准约定不相符;R 类型报错是指该段代码写得极其糟糕,需要重构;W 类型报错是指其触发了 Python 特定的问题;E 类型报错是指代码中可能存在的语法等错误;接下来我们细看 pylint 的报告:
- 首先会报告 pylint 所检查文件的模块、类、方法和函数的数量:
- 接着会报告文件中代码、注释等部分占比,是否有重复行,汇总之前列出的错误数量,最终给出对文件的评分:
Link to original一切基于
pylintrc
pylint 进行检查的一切根据都是来自
pylintrc
这个配置文件,这也是它 high customized 特性的来源,因此建议使用普遍认可的pylintrc
文件,比如 Google 给出的:pylintrc
除了 Python 的 pylint ,其他语言也有对应的静态分析工具:
代码性能分析
代码优化是对代码进行等价变换,使得变换后的代码运行结果与变换前的代码 运行结果相同,但执行速度加快或存储开销减少。
代码性能优化是复杂、繁琐的工程,甚至根据经验,实现程序的重构、优化、 扩展以及文档相关的事情通常需要消耗 80% 的工作量。
代码性能优化应当遵循的规则
- 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率
- 以提高程序的全局效率为主,提高局部效率为辅
- 在优化程序效率时,应先找出限制效率的“瓶颈”
- 先优化数据结构和算法,再优化执行代码
- 时间效率和空间效率可能是对立的,应当分析哪一个因素更重要,再做出适当的折中
- 从一开始就要考虑程序性能,不要期待在开发结束后再做一些快速调整
- 正确的代码要比速度快的代码重要,任何优化都不能破坏代码的正确性
- 认真选择测试数据,使其能够代表实际的使用状况
- 永远不要在没有执行前后性能评估的情况下尝试对代码进行优化
综合以上规则,我们得到这样的优化流程图:
案例分析
问题描述:读入一个文本文件,统计在该文本文件中每个英文单词出现的频率,并输出单词频率最高的100个单词。其中,单词的定义是连续的若干个小写英文字母。
单词示例:
- as 是一个单词
- as, asd 是两个单词
- sa, fdf. fdf fdfdf 是四个单词
编写程序:
仔细审视这段代码,整个程序只需要运行 SplitWords
这一个函数,而这个函数中又分为四个阶段,分别是读入文件、分割单词、统计词频、排序输出,
- 其中读文件、分割单词、排序都是 Python 内置函数/模块完成的原子化步骤,因此本已经过开发者的极致优化,我们在项目中直接使用即可,自己优化起来很麻烦、也不容易实现;
- 但是统计词频时,我们用到了字典类型的
.keys
方法,经过查阅资料,得知每调用一次 keys ()函数,系统就会生成一个新的字典迭代器,这就是额外的开销,我们可以通过直接使用 in 操作符代替之,不再每次生成新的迭代器; - 这也提示我们,优先完成有能力做、又影响性能明显的优化工作;
不过上面的操作其实并不准确,我们是在用自己的知识强行估计每个操作的时间复杂度,这在复杂程序中可行性不高,因此我们需要借助性能分析工具来详细地分析:
- Profile 是 Python 语言内置的性能分析工具,它能够有效地描述程序运行的性能状况,提供各种统计数据帮助程序员找出程序中的性能瓶颈;
- 详细的文档参考:Profile-Get-Started ;
我们可以在上述代码中如下添加、以实现对各步骤的耗时统计:
运行该文件,我们可以得到 profile 的输出为:
这个程序读取的文件比较小,因此运行起来几乎不耗时,所以我们重点关注函数调用次数,可以从上面信息看出:
- 词典对象
dic
的keys
方法被调用了多达 58 次; - 匿名函数
lambda dic:dic[1]
用于提取词典中每个元组的第二个元素(词频),被调用了 49 次; - 在排序时,会隐含地创建列表并追加元素进去,因此
append
方法被调用了 30 次; - 其它调用比较多的,大多是 built-in 方法等不容易修改的方法,我们不必考虑;
因此,这样的数据与我们的预估相符,keys
方法不正确地多次调用,是这个文件额外耗时的关键原因。
profile 中每一列数据的具体含义
- ncalls 函数的被调用次数
- tottime 函数总计运行时间,这里除去函数中调用的其他函数运行时间
- percall 函数运行一次的平均时间,等于 tottime/ncalls
- cumtime 函数总计运行时间,这里包含调用的其他函数运行时间
- percall 函数运行一次的平均时间,等于 cumtime/ncalls
- filename:lineno(function) 函数所在的文件名,函数的行号,函数名
Python 性能优化的一些原则
- 性能优化的关键是如何发现问题,寻找解决问题的方法
- 有效的测试是不可缺少的,通过测试找出真正的瓶颈,并分析优化结果
- 要避免不必要的优化,避免不成熟的优化,不成熟的优化是错误的来源
- 改进算法,选择合适的数据结构是常用手段:
- 对成员的查找访问等操作,字典(dictionary)要比列表(list)更快
- 集合(set)的并、交、差的操作比列表(list)的迭代要快
- 循环优化的基本原则:尽量减少循环过程中的计算量,在多重循环的时候,尽量将内层的计算提到上一层。
- 字符串的优化:
- Python 的字符串对象是不可改变的
- 字符串连接尽量使用
join()
而不是 + - 当对字符串可以使用正则表达式或者内置函数处理时,选择内置函数
- 使用列表解析和生成器表达式:
- 列表解析要比在循环中重新构建一个新的 list 更为高效,因此可以利用这一特性来提高运行的效率
结对编程
- 结对编程是由两名程序员在同一台电脑上结对编写解决同一问题的代码的开发方式,二人互相补充、共同协作;
- 类似汽车比赛中驾驶员与领航员的角色,一者负责编写程序,另一者负责全局思路、纠错、提醒等;
- Pair programming - Wikipedia
Practice
编程作业
请用 python3 编写程序,它可以实现对一个大容量英文文献进行分词与分句,并且能够对该文献内容的全文单词位置进行检索。更具体地,对于一个含有以分隔符(逗号“,”、空格“ ”、分号“;”、英文句号“.”等非英文字母)分隔开的若干单词的文本文献(其中单词可能重复),程序要读入和存储整个文本,并根据输入的若干个单词进行查询,返回每个单词出现的所有句子以及是句子中第几个单词。
1. 功能实现要求(90分)
-
实验输入为一个含有标点符号的英文文献。其中,将所有连续的英文字母视作一个单词,仅以句号“.”、问号“?”、感叹号“!”结尾的才视作一个完整的句子。例如:“fdafa”、“a”、“b”均是一个单词,而“I‘m a boy.”是一个句子,它含有“I”、“m”、“a”、“boy”四个单词;但“I am a boy,”则不是一个完整的句子,因为其以逗号“,”结尾。若最后一句话没有结束符号,则视为不完整的句子,不计入结果。
-
实验中所有的单词字母不区分大小写,“Single”和“single”视为同一个单词。
-
为保证实验公平性,所有实验程序均使用 python3 编写,不允许使用诸如 python 中内置 dict 等哈希表扩展。
-
要求能够从命令行读取文本文件名,代码中以文件的方式读取文献文件 document.txt 和查询文件 query.txt。如若待分析文件名为 document.txt,查询文件名为 query.txt,程序应能够以 python3 sample.py document.txt query.txt 的形式运行,其中 sample.py 是提交的 python 程序文件名。
-
待分析文件中包含完整的英文文献。查询文件中每行一个单词,要求输出这个单词在文献中出现的所有句子的次序以及在该句子中出现的位置。两个数以“/”号隔开,例如第一个句子第二个单词,输出应为1/2。每一个这样的数对之间以逗号隔开,每个单词的所有出现位置输出一行。
-
若待查询单词在文献中没有出现,则输出字符串“None”。
2. 性能测试要求(10分)
在完成功能要求的前提下,本作业还要对代码性能进行测试。具体测试样例如下:
-
性能测试样例1:输入文件大小为1M,待查询单词为100个。
-
性能测试样例2:输入文件大小为5M,待查询单词为1000个。
-
性能测试样例3:输入文件大小为30M,待查询单词为2000个。
性能测试最低要求机器内存小于100M,程序运行时间小于300s,否则视为测试失败。
3. 输入输出样例
【样例一】
文件内容:
I’m a coder in the University.
查询内容:
A
The
输出:
1/3
1/6
【样例二】
文件内容:
She is a beautiful girl. And they met in the school.
查询内容:
BoY
And
Scho
输出:
None
2/1
None
【样例三】
文件内容:
Python is a good language. C++ is another one. But my favourite language is Java. Language is
查询内容:
Language
C
good
输出:
1/5,3/4
2/1
1/4