统一的编程规范为何重要?
本文为景霄-Python核心技术与实战的学习笔记,如要查看完整内容请点击链接。
统一的编程规范可以提高开发效率,开发效率涉及三类对象,即:阅读者、编程者和机器。三者的优先级是:阅读者的体验>>编程者的体验>>机器的体验。
阅读者的体验 高于 编程者
在代码编写过程中,我们更多的时间是在阅读代码而不是写代码。代码质量的高低决定了别人是否可以很容易地读懂你的代码,从而节省开发时间。
如下述代码所示,变量名是否具有确定的含义直接影响着阅读体验。
1 | # 错误示例 |
在Google Style 2.2条中规定,Python代码中的import对象只能是package或者module。
1 | # 错误示例 |
通过导入包或者模块,在调用包中的内容时,我们可以很容易知道所调用的内容的上下文环境和来源。
编程者的体验 高于 机器
在编写代码时,我们经常会不自觉地追求极简的代码编写方式,例如盲目使用Python中的列表表达式:
1 | # 错误示例 |
实际上,类似的代码不仅具有较高的编写难度,同时也不利于代码的阅读这阅读。更简单的做法是:
1 | # 正确示例 |
机器的体验也很重要
编写的代码最终需要在机器上运行,因而代码的效率也很重要。
is
和==
的区别,如下述代码所示:
1 | # 错误示例 |
is
是比较内存地址的,因而第一个输出True
,第二个输出False
。原因在于,CPython将-5到256的整数存放在固定的内存空间中,指向这一范围内的同一个整数对象的变量会指向同一块内存地址。
但对于这个范围之外的整数,重新定义意味着重新分配内存,因而不同变量的地址自然不同。
因而,即使
is
比较对象的内存地址,也应该尽量避免使用is
比较两个Python整数的地址。
因而比较整数对象应尽量使用:
1 | # 正确示例 |
而在使用==
时同样也应该注意:
1 | # 错误示例 |
上述代码的执行结果取决于MyObject()
的__eq__()
方法的具体实现。正确的做法是,在和None
进行比较的时候,要使用is
:
1 | # 正确示例 |
但是除此之外还要注意Python中的隐式布尔转换,比如:
1 | # 错误示例 |
当执行pay("Andrew", 0)
时,0会被自动转换为布尔值,因而输出结果为:”Ahdrew is compensated 11 dollers”。因而,当明确想要比较对象是否是None时,一定要使用is None
。
1 | # 正确示例 |
同时,不规范的编程习惯会导致程序效率问题。
1 | # 错误示例 |
该代码的keys()
方法会在遍历前生成一个临时的列表,导致消耗大量的内存并且运行缓慢。正确的做法是使用默认的iterator。默认的iterator不会重新分配内存。
1 | # 正确示例 |
PEP 8规范
PEP是Python Enhancement Proposal的缩写,即“Python增强规范”。其存在意义就是让Python更易阅读。
缩写规范
Python和C++ / Java的不同之处在于,后者使用大括号区分代码块,而Python完全使用不同行和不同的缩进来进行分块。
在进行缩进时,有多种选择,Tab、双空格、四空格、空格和Tab混合等。而PEP8的要求是:
使用四个空格的缩进,不要使用Tab,更不要使用Tab和空格的混用。
同时,每一行代码的长度要限制在79个字符以内。
这一规定有以下两个有点:
- 当一行代码的字符数过多时,不利于程序员的阅读;
- 当代码的嵌套层数过高时,比如超过三层之后,一行的内容很容易就会超过79个字符。因而,该规定也在另一方面约束程序员,不要使用过深的约束,而是要把代码继续分解为其他函数或者逻辑块,对代码的结构进行优化。
空行规范
PEP 8规定:
全局的类和函数的上方需要空两个空行,而类的函数之间需要空一个空行。同时,函数的内部也可以使用空行来区分不同的代码块,但最多只能使用一行。
同时,在代码的尾部,每个代码文件的最后一行为空行,并且只有一个空行。
空格规范
在函数的参数列表中,每一个参数的逗号后都要跟一个空格。目的在于使得每个参数都能够独立阅读。同理,冒号经常被用来初始化字典,因而冒号后也要跟一个空格。
在使用#进行单行注释时,要在#后、注释前添加一个空格。
另外,在操作符,例如+, -, *, /等的两端都要保留空格。但是,括号的两端不需要空格。
换行规范
以如下代码为例:
1 | def solve1(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter, |
在进行代码换行时,有两种经典的做法。
第一种:通过括号将过长的运算进行封装。此时虽然跨行,但是仍处于一个逻辑引用之内。对于solve1
函数,由于参数过多,因而需要进行换行,但要注意的是第二行的参数要和函数的第一个参数对齐,这样做可以使得函数变得美观。
第二种:使用换行符实现。
文档规范
在使用import
语句时,要尽量将其放在文件的开头。同时,不要在同一行中一次导入过个模块,例如:不要使用import time, os
。
注释规范
对于大的逻辑块,可以在最开始相同的缩进处以#开始写注释。对于行注释,可以在一行后面跟两个空格,接着以#开头加入注释。但是,并不推荐使用行注释。
1 | # This is an example to demonstrate how to comment. |
文档描述
以TensorFlow的代码为例。
1 | class SpatialDropout2D(Dropout): |
类和函数的注释为的是让读者快速理解函数的作用,以及输入参数、输出的返回值和格式以及其他需要注意的地方。
而docstring的写法是以三个双引号开始,三个双引号结束。首先用一句话概述函数的所用,接着用一段话详细解释;接着是参数列表、参数格式和返回值格式。
命名规范
变量命名拒绝使用毫无意义的单字符。应该使用能改代表其意思的变量名,一般,变量使用小写,通过下划线串联。例如:data_format、input_spec等。唯一可以使用单字符的地方是迭代,如for i in range(n)
。同时,如果是类的私有变量,要在变量前加两个下划线。
常量,最好的做法是全部大写。而对于函数名,同样使用小写的方式,通过下划线连接。如:launch_nuclear_missile()
、check_input_validation()
。
类名,应该首字母大写,然后合并起来,例如:class SpatialDropout2D()
。
代码分解技巧
编程的一个核心思想是不写重复代码,重复代码可以使用条件、循环、构造函数和类解决;另一个核心思想是减少迭代层数,尽可能让Python代码扁平化。
在业务逻辑比较复杂的地方,需要加入大量的判断和循环。但写好判断和循环需要注意很多细节问题。
1 | if i_am_rich: |
上述代码中,send(mony)
出现了两次,因而可以对其进行合并。
1 | if i_am_rich: |
再看下面一个例子:
1 | def send(money): |
这段代码中使用了过多的缩进,改进如下:
1 | def send(money): |
同时,在编写函数时,一个函数的功能应尽可能地细,单个函数不应该承担过多的功能。对于一个复杂的函数,应该将其拆分为几个功能简单的函数,然后进行合并。
以二分搜索为例,给定一个非递减整数数组和一个target,要求找到数组中最小的一个数x,满足x*x>target,如果不存在则返回-1。
首先给出代码如下:
1 | def solve(arr, target): |
可以继续优化如下:
1 | def comp(x, target): |
在上述改进代码中,不仅对功能进行了拆分,同时对二分搜索的主程序进行了提取,使其只负责二分搜索。
讲完了函数的拆分,下面来看看如何对类进行拆分。
1 | class Person: |
在上述类中,job
相关的属性有很多,并且表达的是同一个意义的实体,因而可以将这部分代码独立出来,作为独立的类。
1 | class Person: |
如何合理使用assert?
什么是assert?
Python的assert
主要被用于测试一个条件是否满足,如果满足则不执行任何语句;如果不满足则抛出AssertionError
,并返回具体的错误信息(可选)。
具体语法如下:
1 | assert_stmt ::= "assert" expression ["," expression] |
例如:
1 | assert 1 == 2 |
相当于如下代码:
1 | if __debug__: |
还可以使用如下形式:
1 | assert 1 == 2, 'assertion is wrong' |
相当于:
1 | if __debug__: |
这里的debug是一个常数。如果 Python 程序执行时附带了-O这个选项,比如Python test.py -O,那么程序中所有的 assert 语句都会失效,常数debug便为 False;反之debug则为 True。要注意的是,不能直接对debut赋值,这一操作是非法的。
同时,在使用assert
时不能加入括号:
1 | assert(1 == 2, 'This should fail') |
assert 在程序中的作用,是对代码做一些 内部 的自检。使用 assert,就表示很确定。这个条件一定会发生或者一定不会发生。
assert的用法
假设在某个活动中需要进行打折促销,需要一个函数,输入为原来的价格和折扣,输出为最后的价格。那么可以编写代码如下:
1 | def apply_discount(price, discount): |
此时,进行如下测试:
1 | apply_discount(100, 0.2) |
又以除法操作为例:
1 | def calculate_average_price(total_sales, num_sales): |
除此之外,还有一些很常见的做法:
1 | def func(input): |
错误用法
假如,某个课程平台需要删除上线时间过长的一些专栏,于是有如下的专栏删除函数:
1 | def delete_course(user, course_id): |
在代码角度,这段代码没有问题。但是,在实际工程中是有问题的,一旦assert的检查被关闭,两个检查语句便会被跳过,导致程序运行异常。正确的做法是使用条件语句进行相应的检验:
1 | def delete_course(user, course_id): |
总的来说,assert 并不适用 run-time error 的检查。比如你试图打开一个文件,但文件不存在;再或者是你试图从网上下载一个东西,但中途断网了等等。