27 | 学会合理分解代码,提高代码可读性
下载APP
关闭
渠道合作
推荐作者
27 | 学会合理分解代码,提高代码可读性
2019-07-10 景霄 来自北京
《Python核心技术与实战》
课程介绍
讲述:冯永吉
时长14:06大小12.90M
你好,我是景霄。今天我们不讲任何技术知识点,继续来一起聊聊代码哲学。
有句话说得好,好的代码本身就是一份文档。同样功能的一份程序,一个组件,一套系统,让不同的人来写,写出来的代码却是千差万别。
有些人的设计风格和代码风格犹如热刀切黄油,从顶层到底层的代码看下来酣畅淋漓,注释详尽而又精简;深入到细节代码,无需注释也能理解清清楚楚。
而有些人,代码勉勉强强能跑起来,遇到稍微复杂的情况可能就会出 bug;深入到代码中 debug,则发现处处都是魔术数、函数堆在一起。一个文件上千行,设计模式又是混淆不堪,让人实在很难阅读,更别提修改和迭代开发。
Guido van Rossum(吉多·范罗苏姆,Python 创始人 )说过,代码的阅读频率远高于编写代码的频率。毕竟,即使是在编写代码的时候,你也需要对代码进行反复阅读和调试,来确认代码能够按照期望运行。
话不多说,进入正题。
PEP 8 规范
上节课我们简单提起过 PEP 8 ,今天我们继续来详细解读。
PEP 是 Python Enhancement Proposal 的缩写,翻译过来叫“Python 增强规范”。正如我们写文章,会有句式、标点、段落格式、开头缩进等标准的规范一样,Python 书写自然也有一套较为官方的规范。PEP 8 就是这样一种规范,它存在的意义,就是让 Python 更易阅读,换句话,增强代码可读性。
事实上,Pycharm 已经内置了 PEP 8 规范检测器,它会自动对编码不规范的地方进行检查,然后指出错误,并推荐修改方式。下面这张图就是其界面。
因此,在学习今天的内容时,我推荐你使用 Pycharm IDE 进行代码检查,看一下自己的代码格式哪里有问题。尤其对于初学者,从某些程度来说,代码规范甚至是比代码准确更重要的事情,因为实际工作中,代码可读性的重要性一定比你想象的多得多。
缩进规范
首先,我们来看代码块内的缩进。
Python 和 C++ / Java 最大的不同在于,后者完全使用大括号来区分代码块,而前者依靠不同行和不同的缩进来进行分块。有一个很有名的比赛,叫作 C 语言混乱代码大赛,其中有很多非常精彩的作品,你能看到书写的代码排成各种形状,有的是一幅画,或者一个卡通头像,但是能执行出惊人的结果。
而放到 Python ,显然就不能实现同样的技巧了。不过,以小换大,我们有了“像阅读英语”一样清晰的 Python 代码,也还是可以接受的。
话说回来,Python 的缩进其实可以写成很多种,Tab、双空格、四空格、空格和 Tab 混合等。而 PEP 8 规范告诉我们,请选择四个空格的缩进,不要使用 Tab,更不要 Tab 和空格混着用。
第二个要注意的是,每行最大长度请限制在 79 个字符。
这个原则主要有两个优点,第一个优点比较好理解。很多工程师在编程的时候,习惯一个屏幕并列竖排展示多个源代码。如果某个源代码的某些行过长,你就需要拖动横向滚动条来阅读,或者需要软回车将本行内容放入下一行,这就极大地影响了编码和阅读效率。
至于第二个优点,需要有一定经验的编程经验后更容易理解:因为当代码的嵌套层数过高,比如超过三层之后,一行的内容就很容易超过 79 个字符了。所以,这条规定另一方面也在制约着程序员,不要写迭代过深的代码,而是要思考继续把代码分解成其他函数或逻辑块,来优化自己的代码结构。
空行规范
接着我们来看代码块之间的空行。
我们知道,Python 中的空行对 Python 解释器的执行没有影响,但对阅读体验有很深刻的影响。
PEP 8 规定,全局的类和函数的上方需要空两个空行,而类的函数之间需要空一个空行。当然,函数内部也可以使用空行,和英语的段落一样,用来区分不同意群之间的代码块。但是记住最多空一行,千万不要滥用。
另外,Python 本身允许把多行合并为一行,使用分号隔开,但这是 PEP 8 不推荐的做法。所以,即使是使用控制语句 if / while / for,你的执行语句哪怕只有一行命令,也请另起一行,这样可以更大程度提升阅读效率。
至于代码的尾部,每个代码文件的最后一行为空行,并且只有这一个空行。
空格规范
我们再来看一下,代码块中,每行语句中空格的使用。
函数的参数列表中,调用函数的参数列表中会出现逗号,请注意逗号后要跟一个空格,这是英语的使用习惯,也能让每个参数独立阅读,更清晰。
同理,冒号经常被用来初始化字典,冒号后面也要跟一个空格。
另外,Python 中我们可以使用#进行单独注释,请记得要在#后、注释前加一个空格。
对于操作符,例如+,-,*,/,&,|,=,==,!=,请在两边都保留空格。不过与此对应,括号内的两端并不需要空格。
换行规范
现在再回到缩进规范,注意我们提到的第二点,控制每行的最大长度不超过 79 个字符,但是有时候,函数调用逻辑过长而不得不超过这个数字时,该怎么办呢?
请看下面这段代码,建议你先自己阅读并总结其特点:
事实上,这里有两种经典做法。
第一种,通过括号来将过长的运算进行封装,此时虽然跨行,但是仍处于一个逻辑引用之下。solve1 函数的参数过多,直接换行,不过请注意,要考虑第二行参数和第一行第一个参数对齐,这样可以让函数变得非常美观的同时,更易于阅读。当然,函数调用也可以使用类似的方式,只需要用一对括号将其包裹起来。
第二种,则是通过换行符来实现。这个方法更为直接,你可以从 solve2 和第二个函数调用看出来。
关于代码细节方面的规范,我主要强调这四个方面。习惯不是一天养成的,但一定需要你特别留心和刻意练习。我能做的,便是告诉你这些需要留心的地方,并带你感受实际项目的代码风格。
下面的代码选自开源库 Google TensorFlow Keras,为了更加直观突出重点,我删去了注释和大部分代码,你意会即可。我希望,通过阅读这段代码,你能更真实地了解到,前沿的项目是怎么在增强阅读性上下功夫的。
文档规范
接下来我们说说文档规范。先来看看最常用的 import 函数。
首先,所有 import 尽量放在开头,这个没什么说的,毕竟到处 import 会让人很难看清楚文件之间的依赖关系,运行时 import 也可能会导致潜在的效率问题和其他风险。
其次,不要使用 import 一次导入多个模块。虽然我们可以在一行中 import 多个模块,并用逗号分隔,但请不要这么做。import time, os 是 PEP 8 不推荐的做法。
如果你采用 from module import func 这样的语句,请确保 func 在本文件中不会出现命名冲突。不过,你其实可以通过 from module import func as new_func 来进行重命名,从而避免冲突。
注释规范
有句话这么说:错误的注释,不如没有注释。所以,当你改动代码的时候,一定要注意检查周围的注释是否需要更新。
对于大的逻辑块,我们可以在最开始相同的缩进处以 # 开始写注释。即使是注释,你也应该把它当成完整的文章来书写。如果英文注释,请注意开头大写及结尾标点,注意避免语法错误和逻辑错误,同时精简要表达的意思。中文注释也是同样的要求。一份优秀的代码,离不开优秀的注释。
至于行注释,如空格规范中所讲,我们可以在一行后面跟两个空格,然后以 # 开头加入注释。不过,请注意,行注释并不是很推荐的方式。
文档描述
再来说说文档描述,我们继续以 TensorFlow 的代码为例。
你应该可以发现,类和函数的注释,为的是让读者快速理解这个函数做了什么,它输入的参数和格式,输出的返回值和格式,以及其他需要注意的地方。
至于 docstring 的写法,它是用三个双引号开始、三个双引号结尾。我们首先用一句话简单说明这个函数做什么,然后跟一段话来详细解释;再往后是参数列表、参数格式、返回值格式。
命名规范
接下来,我来讲一讲命名。你应该听过这么一句话,“计算机科学的两件难事:缓存失效和命名。”命名对程序员来说,是一个不算省心的事。一个具有误导性的名字,极有可能在项目中埋下潜在的 bug。这里我就不从命名分类方法来给你划分了,我们只讲一些最实用的规范。
先来看变量命名。变量名请拒绝使用 a b c d 这样毫无意义的单字符,我们应该使用能够代表其意思的变量名。一般来说,变量使用小写,通过下划线串联起来,例如:data_format、input_spec、image_data_set。唯一可以使用单字符的地方是迭代,比如 for i in range(n) 这种,为了精简可以使用。如果是类的私有变量,请记得前面增加两个下划线。
对于常量,最好的做法是全部大写,并通过下划线连接,例如:WAIT_TIME、SERVER_ADDRESS、PORT_NUMBER。
对于函数名,同样也请使用小写的方式,通过下划线连接起来,例如:launch_nuclear_missile()、check_input_validation()。
对于类名,则应该首字母大写,然后合并起来,例如:class SpatialDropout2D()、class FeatureSet()。
总之,还是那句话,不要过于吝啬一个变量名的长度。当然,在合理描述这个变量背后代表的对象后,一定的精简能力也是必要的。
代码分解技巧
最后,我们再讲一些很实用的代码优化技巧。
编程中一个核心思想是,不写重复代码。重复代码大概率可以通过使用条件、循环、构造函数和类来解决。而另一个核心思想则是,减少迭代层数,尽可能让 Python 代码扁平化,毕竟,人的大脑无法处理过多的栈操作。
所以,在很多业务逻辑比较复杂的地方,就需要我们加入大量的判断和循环。不过,这些一旦没写好,程序看起来就是地狱了。
我们来看下面几个示例,来说说写好判断、循环的细节问题。先来看第一段代码:
这段代码中,同样的 send 语句出现了两次,所以我们完全可以合并一下,把代码改造成下面这样:
再来看一个例子:
这段代码层层缩进,显而易见的难看。我们来改一下:
新的代码是不是就清晰多了?
另外,我们知道,一个函数的粒度应该尽可能细,不要让一个函数做太多的事情。所以,对待一个复杂的函数,我们需要尽可能地把它拆分成几个功能简单的函数,然后合并起来。那么,应该如何拆分函数呢?
这里,我以一个简单的二分搜索来举例说明。我给定一个非递减整数数组,和一个 target,要求你找到数组中最小的一个数 x,可以满足 x*x > target。一旦不存在,则返回 -1。
这个功能应该不难写吧。你不妨先自己写一下,写完后再对照着来看下面的代码,找出自己的问题。
我给出的第一段代码这样的写法,在算法比赛和面试中已经 OK 了。不过,从工程角度来说,我们还能继续优化一下:
你可以看出,第二段代码中,我把不同功能的代码拿了出来。其中,comp() 函数作为核心判断,拿出来后可以让整个程序更清晰;同时,我也把二分搜索的主程序拿了出来,只负责二分搜索;最后的 solve() 函数拿到结果,决定返回不存在,还是返回值。这样一来,每个函数各司其职,阅读性也能得到一定提高。
最后,我们再来看一下如何拆分类。老规矩,先看代码:
你应该能看得出来,job 在其中出现了很多次,而且它们表达的是一个意义实体,这种情况下,我们可以考虑将这部分分解出来,作为单独的类。
你看,改造后的代码,瞬间就清晰了很多。
总结
今天这节课,我们简单讲述了如何提高 Python 代码的可读性,主要介绍了 PEP 8 规范,并通过实例的说明和改造,让你清楚如何对 Python 程序进行可读性优化。
思考题
最后,我想留一个思考题。这次的思考题开放一些,希望你在评论区讲一讲,你自己在初学编程时,不注意规范问题而犯下的错误,和这些错误会导致什么样的后果,比如对后来读代码的人有严重的误导,或是埋下了潜在的 bug 等等。
希望你在留言区分享你的经历,你也可以把这篇文章分享出去,让更多的人互相交流心得体会,留下真实的经历,并在经历中进步成长。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 22
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
26 | 活都来不及干了,还有空注意代码风格?!
下一篇
28 | 如何合理利用assert?
精选留言(33)
- helloworld2019-07-11我的总结 缩进规范: 1. 使用四空格缩进 2. 每行最大长度79个字符 空行规范: 1. 全局的(文件级别的)类和全局的函数上方要有两个空行 2. 类中的函数上方要有一个空行 3. 函数内部不同意群的代码块之前要有一个空行 4. 不要把多行语句合并为一行(即不要使用分号分隔多条语句) 5. 当使用控制语句if/while/for时,即使执行语句只有一行命令,也需要另起一行 6. 代码文件尾部有且只有一个空行 空格规范: 1. 函数的参数之间要有一个空格 2. 列表、元组、字典的元素之间要有一个空格 3. 字典的冒号之后要有一个空格 4. 使用#注释的话,#后要有一个空格 5. 操作符(例如+,-,*,/,&,|,=,==,!=)两边都要有一个空格,不过括号(包括小括号、中括号和大括号)内的两端不需要空格 换行规范: 1. 一般我们通过代码逻辑拆分等手段来控制每行的最大长度不超过79个字符,但有些时候单行代码还是不可避免的会超过这个限制,这个时候就需要通过换行来解决问题了。 2. 两种实现换行的方法: 第一种,通过小括号进行封装,此时虽然跨行,但是仍处于一个逻辑引用之下。比如函数参数列表的场景、过长的运算语句场景 第二种,直接通过换行符(\)实现 文档规范: 1. 所有import尽量放在代码文件的头部位置 2. 每行import只导入一个对象 3. 当我们使用from xxx import xxx时,import后面跟着的对象要是一个package(包对应代码目录)或者module(模块对应代码文件),不要是一个类或者一个函数 注释规范: 1. 代码块注释,在代码块上一行的相同缩进处以 # 开始书写注释 2. 代码行注释,在代码行的尾部跟2个空格,然后以 # 开始书写注释,行注释尽量少写 3. 英文注释开头要大写,结尾要写标点符号,避免语法错误和逻辑错误,中文注释也是相同要求 4. 改动代码逻辑时,一定要及时更新相关的注释 文档描述规范: 三个双引号开始、三个双引号结尾; 首先用一句话简单说明这个函数做什么,然后跟一段话来详细解释; 再往后是参数列表、参数格式、返回值格式的描述。 命名规范: 1. 变量名,要全部小写,多个词通过下划线连接,可以使用单字符变量名的场景,比如for i in range(n)这种变量没有实际意义的情况 2. 类的私有变量名,变量名前需要加2个下划线 3. 常量名,要全部大写,多个词通过下划线连接 4. 函数名,要全部小写,多个词通过下划线连接 5. 类名,要求驼峰形式,即单词首字母大写,多个词的话,每个词首字母大写然后直接拼接 6. 命名需要做到名如其意,不要吝啬名字的长度 代码分解技巧: 1. 不写重复代码,重复代码可以通过使用条件、循环、函数和类来解决 2. 减少迭代层数,让代码扁平化 3. 函数拆分,函数的粒度尽可能细,也就是一个函数不要做太多事情 4. 类的拆分,如果一个类中有多个属性描述的是同一类事物,就可以把这些属性拆分出来新建一个单独的类 5. 模块化,若项目偏大,要为不同功能模块创建独立的目录或文件,通过import互相关联展开
作者回复: 很棒
共 4 条评论110 - lipan2019-07-11还记得刚开始写第一个iOS项目的时候,一个文件几千行代码。 业务和逻辑没有分层设计的思想,全部混合在一个文件里,虽然勉强实现了功能,但是后期代码没法改,连我自已都看不懂了,遑论接盘者。 后来在做新浪微博登录授权的时候,看了一下微博oAuth的授权流程,顿时大开眼界。后来又看了一些苹果给的开发例子,才开始有了代码的分层设计和想法,然后才开始学习iOS的MVC和MVVM的设计架构,代码算是勉强看得下去。 所以我觉得如果要提高代码水平,除了动手实战以外,还要多观摹和学习大神的项目代码。 看到老师能够把Python知识给我们讲解深入浅出,举重若轻,完全符合我心目中的大神风范。 因此,希望老师可以给我们分享一些Python的小、中型的项目或Demo(关于爬虫、机器学习、深度学习、自动运营、量化分析或是其它有趣的Python做的小工具都可以)。以供我们观摹学习。谢谢。展开
作者回复: 👍
30 - 夜路破晓2019-07-10听很多程序员讲过,开始他们的关注点大多数是先写出能跑起来的代码,后期当优化成为他们的瓶颈和需求时再来关注代码规范之类的问题。 对于初学者而言,想要实现弯道超车,就需要下大力气把基础夯实,而代码规范正是其中重要的一项。 不可否认,担心学了半天能写漂亮但跑不起来代码的大有人在。如何权衡呢? 分享一个认知:写漂亮代码与写能跑起来的代码之间不存在因果关系,两者都是你需要花费时间精力学习的内容。 越早认识到这一点,越能合理而高效地安排自己的学习计划。展开
作者回复: 万事万物都有个度在里面。 在公司写业务逻辑,有规定完成时间的前提下,很容易写出不规范,不优雅的代码,长期以往,能力自然得不到更多的锻炼。自己学习的时候,就一定不要为了贪图快,而是更多地去进行思考,之后再写代码,多花时间在思考上,而不是打字上。
共 2 条评论11 - new2019-07-10如果代码逻辑清晰,加上注释是锦上添花的事,相对写代码和读代码,程序员更愿意自己写代码,而在项目中又必须要读代码,因此代码注释真的很重要,特别是那些逻辑复杂还掺杂部分业务的代码逻辑,如果不加注释几乎不可能有人理解,但是问题来了,如果后来的程序员把最初编写代码的人的代码修改了,但是注释却不更新就会对公司其他人产生重大影响相当于埋了雷,比如你用128个标识位分别表示128个探头启停信号,而在硬件升级过程中探头数量减少了4个探头,原始注释可能是描述128个探头信号启停等信息,而第二个程序员在升级修改时直接修改了循环计数变量为124,常量128未修改,注释也没有改,后又新来的程序员以为程序存在bug,把循环次数重新改回了常量定义128,导致4个探头怎么也读取不到信号,测试n长时间,最后查到硬件才明白是根本没有那4个探头展开
作者回复: 👍
共 2 条评论6 - Leon📷2019-11-12大量if else嵌套我接盘过这类代码,我觉得有两个原因造成的,第一就是开发者自己逻辑不清晰,想到哪写到哪 第二 就是懒,赶紧把泥巴墙糊完去结工钱的感觉,这是对项目的极度不负责任,也是对自己的不负责任,坏的习惯养成了就很难改掉了3
- Geek_d848f72019-07-15出现过命名和内置模块一样的情况,结果调用内置模块时,报错或者不起作用2
- Geek.S.2020-03-30python 代码规范中,实践中最难的是变量命名,每次起变量名都花比较长时间,待完成代码编写后,经常需要修改变量名或函数名,使得变量名能够见名知意; 又或者过了一段时间后,发现函数名还是不够精确,于是改了,为了使得使用的代码不会保存,又将旧名字指向新名字。 使用 pycharm 编写代码,我很少会按 ctrl + alt + L,而是根据页面波浪线的提示,手动调整,长年累月,不仅知道了代码规范的具体规则,还可以在脱离 IDE 环境下写出符合代码规范的代码,比如在服务器 vim 里写代码,现在看来这个习惯很受益。 不止对代码规范,连码子都要遵循中文写作规范,哈哈,感觉有点强迫症。展开2
- 吴月月鸟2019-07-11写的代码没写注释,当初自己一行一行码的代码,过段时间自己都不认识了。1
- JackDong2019-07-10老师,我看之前的文章有个疑问,就是说列表在扩容的时候如果遇上空间被占用,需要重新复制到一块新的地址时。它的id会变么?1
- 孙楚2022-05-31为什么不建议使用#呢
- 三件事2021-01-12太喜欢这一篇了,不但讲了该怎么样,而且给出了具体的反例。看起来非常清晰明了。老师的这个专栏太值了,始于 Python 而不止于 Python,这是我看到目前最大的体会。👍👍👍
- Geek rick2020-11-30目前就是在读一堆千行以上的文件的代码, 读得头疼, 重构ing
- 皮特尔2020-06-17曾经维护过一个上千行的for循环,很酸爽
- 王大华2020-04-22使用pycharm给函数rename,误伤了一部分函数,系统崩掉部分功能,害怕共 1 条评论
- Paul Shan2019-11-22代码拆分是让代码清晰的关键,毕竟很少有特别大部头的代码是可读的。
- Paul Shan2019-11-22代码规范应该由机器自动完成,当一个程序员使用多种语言的时候,每种语言的规范其实不一样,例如命名规则,如果都有机器自动完成检查,事半功倍。
- 大象2019-10-29有推荐的“目录结构规范”么? 谢谢
- 自由民2019-10-19初学时很多bug找了半天不知道问题在哪,最后发现是少加1或者多加1这类的,或者少写一个=。而之所以看不出来,跟代码写了并拢在一起,没有适当的空格很有关系。那时又是初学,很打击信心。后来就严格按规范写了。
- 丁丁历险记2019-10-07重构,改善代码即有设计,里面有大量拆解套路,例如代码意图和代码实现分离。
- djfhchdh2019-09-26现在越来越觉得给变量、函数起名字是个大学问~~~~,最重要的是不能有歧义!!!!