规范修炼-Google编程规范解读
Guido van Rossum(吉多·范罗苏姆,Python 创始人 )说过,代码的阅读频率远高于编写代码的频率。毕竟,即使是在编写代码的时候,你也需要对代码进行反复阅读和调试,来确认代码能够按照期望运行。
接下来我们今天就为大家分享《Python编程规范:让你的代码脚下生风》第三弹《Google开源项目风格指南》
Python是Google主要的脚本语言。这本风格指南主要包含的是针对python的编程规范。
Google开源项目风格指南-Python风格指南包含以下两个主要内容
Python风格规范
Python语言规范
文档地址: Google开源项目风格指南
一、Python风格规范
在Google的Python风格规范中主要涉及了像如何使用缩进、空格和代码行的长度、注释、文档以及老生常谈的命名规范、分号的使用、导入格式等,这些基本上在前面的分享中都提及过,这里就不在过多解释。
但是在Google风格规范中提及了Main
的使用,我认为这个还是比较重要的内容。
因此我们在模块文件或者自定义模块时中总是能够看到或使用下面这样的语句。
def main():
print('run')
if __name__ == '__main__':
main()
‘’‘
__name__ 是当前模块名,当模块被直接运行时模块名为 ‘__main__’
一般我们会在模块测试时,把相关的调用或者执行放在`if __name__ == '__main__'`当中。
这样可以保证我们的测试代码不会在模块被导入时执行,而只是在模块被作为主程序时执行。
’‘’
这是一种很好的特性,而我们在进行模块开发时也变的更加方便,那么接下来我们看一下在Google编码规范中时如何对Main的使用进行规范化的。
Main
即使是一个打算被用作脚本的文件, 也应该是可导入的. 并且简单的导入不应该导致这个脚本的主功能(main functionality)被执行, 这是一种副作用. 主功能应该放在一个main()函数中.
在Python中, pydoc以及单元测试要求模块必须是可导入的.
你的代码应该在执行主程序前总是检查 if __name__ == '__main__'
,
这样当模块被导入时主程序就不会被执行.
def main():
print('run')
if __name__ == '__main__':
main()
所有的顶级代码在模块导入时都会被执行.
要小心不要去调用函数, 创建对象, 或者执行那些不应该在使用pydoc时执行的操作.
二、 Python语言规范
接下来我们一起看下《Google开源项目风格指南》中对Python语言的一些规范吧
1.Lint
对你的代码运行pylint,pylint是一个在Python源代码中查找bug的工具.
可以捕获容易忽视的错误, 例如输入错误, 使用未赋值的变量等.
2.导入
仅对包和模块使用导入.并且必要时使用as
3.包
使用模块的全路径名来导入每个模块
4.异常
允许使用异常, 但必须小心。
总结几点如下:
- 永远不要使用 except: 语句来捕获所有异常, 也不要捕获 Exception 或者 StandardError , 除非你打算重新触发该异常, 或者你已经在当前线程的最外层(记得还是要打印一条错误消息). 在异常这方面, Python非常宽容, except: 真的会捕获包括Python语法错误在内的任何错误. 使用 except: 很容易隐藏真正的bug.
- 尽量减少try/except块中的代码量. try块的体积越大, 期望之外的异常就越容易被触发. 这种情况下, try/except块将隐藏真正的错误.
- 使用finally子句来执行那些无论try块中有没有异常都应该被执行的代码. 这对于清理资源常常很有用, 例如关闭文件.
5.全局变量
避免全局变量,导入时可能改变模块行为, 因为导入模块时会对模块级变量赋值.
避免使用全局变量, 用类变量来代替. 但也有一些例外:
- 脚本的默认选项.
- 模块级常量. 例如: PI = 3.14159. 常量应该全大写, 用下划线连接.
- 有时候用全局变量来缓存值或者作为函数返回值很有用.
- 如果需要, 全局变量应该仅在模块内部可用, 并通过模块级的公共函数来访问.
6.嵌套/局部/内部类或函数
鼓励使用嵌套/本地/内部类或函数
定义:
- 类可以定义在方法, 函数或者类中.
- 函数可以定义在方法或函数中.
- 封闭区间中定义的变量对嵌套函数是只读的.
优点: 允许定义仅用于有效范围的工具类和函数.
缺点: 嵌套类或局部类的实例不能序列化(pickled).
结论: 推荐使用.
7.列表推导(List Comprehensions)
可以在简单情况下使用,复杂的列表推导或者生成器表达式可能难以阅读.
先看结论
简单的列表推导可以比其它的列表创建方法更加清晰简单.
但是复杂的列表推导或者生成器表达式可能难以阅读.
因此
列表推导适用于简单情况. 每个部分应该单独置于一行: 映射表达式, for语句, 过滤器表达式.
禁止多重for语句或过滤器表达式. 复杂情况下还是使用循环.
下面是一些代码示例,可以感受一下
# 适用于简单情况.
# 每个部分应该单独置于一行: 映射表达式, for语句, 过滤器表达式.
# 禁止多重for语句或过滤器表达式. 复杂情况下还是使用循环.
# Yes:
result = []
for x in range(10):
for y in range(5):
if x * y > 10:
result.append((x, y))
for x in xrange(5):
for y in xrange(5):
if x != y:
for z in xrange(5):
if y != z:
yield (x, y, z)
# No:
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]
return ((x, y, z)
for x in xrange(5)
for y in xrange(5)
if x != y
for z in xrange(5)
if y != z)
8.默认迭代器和操作符
如果类型支持, 就使用默认迭代器和操作符. 比如列表, 字典及文件等.
优点:
默认操作符和迭代器简单高效, 它们直接表达了操作, 没有额外的方法调用.
使用默认操作符的函数是通用的. 它可以用于支持该操作的任何类型.
缺点:
- 你没法通过阅读方法名来区分对象的类型(例如, has_key()意味着字典). 不过这也是优点.
# 内建类型也定义了迭代器方法. 优先考虑这些方法, 而不是那些返回列表的方法.
# 当然,这样遍历容器时,你将不能修改容器.
# Yes:
for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in dict.iteritems(): ...
# No:
for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
9.生成器
按需使用生成器.
生成器的定义:
所谓生成器函数, 就是每当它执行一次生成(yield)语句, 它就返回一个迭代器, 这个迭代器生成一个值.
生成值后, 生成器函数的运行状态将被挂起, 直到下一次生成.
优点:简化代码, 因为每次调用时, 局部变量和控制流的状态都会被保存. 比起一次创建一系列值的函数, 生成器使用的内存更少.
鼓励使用. 注意在生成器函数的文档字符串中使用”Yields:”而不是”Returns:”.
10.Lambda函数
适用于单行函数.
优点:方便。
缺点:比本地函数更难阅读和调试. 没有函数名意味着堆栈跟踪更难理解. 由于lambda函数通常只包含一个表达式, 因此其表达能力有限.
结论:适用于单行函数. 如果代码超过60-80个字符, 最好还是定义成常规(嵌套)函数.
11.条件表达式
条件表达式是对于if语句的一种更为简短的句法规则. 例如: x = 1 if cond else 2
.
优点:比if语句更加简短和方便.
缺点:比if语句难于阅读. 如果表达式很长, 难于定位条件.
结论:适用于单行函数. 在其他情况下,推荐使用完整的if语句.
12.默认参数值
适用于大部分情况。
# 不要在函数或方法定义中使用可变对象作为默认值.
Yes: def foo(a, b=None):
if b is None:
b = []
No: def foo(a, b=[]):
...
No: def foo(a, b=time.time()): # The time the module was loaded
...
No: def foo(a, b=FLAGS.my_thing): # sys.argv has not yet been parsed...
...
结论:鼓励使用,但需要注意:不要在函数或方法定义中使用可变对象作为默认值.
13.True/False的求值
尽可能使用隐式false,
Python在布尔上下文中会将某些值求值为false. 按简单的直觉来讲, 就是所有的”空”值都被认为是false.
因此0, None, [], {}, “” 都被认为是false.
优点: 使用Python布尔值的条件语句更易读也更不易犯错. 大部分情况下, 也更快.
结论: 尽可能使用隐式的false, 例如: 使用 if foo:而不是 if foo != []:
不过还是有一些注意事项需要你铭记在心:
# 1. 永远不要用==或者!=来比较单件, 比如None. 使用is或者is not.
# 2. 注意: 当你写下 `if x:` 时, 你其实表示的是 `if x is not None` .
# 3. 永远不要用==将一个布尔量与false相比较. 使用 `if not x:` 代替.
# 如果你需要区分false和None, 你应该用像 `if not x and x is not None:` 这样的语句.
# 4. 对于序列(字符串, 列表, 元组), 要注意空序列是false.
# 因此 `if not seq:` 或者 `if seq:` 比 `if len(seq):` 或 `if not len(seq):` 要更好.
# 5. 处理整数时, 使用隐式false可能会得不偿失(即不小心将None当做0来处理).
# 你可以将一个已知是整型(且不是len()的返回结果)的值与0比较.
# Yes:
if not users:
print 'no users'
if foo == 0:
self.handle_zero()
if i % 10 == 0:
self.handle_multiple_of_ten()
#No:
if len(users) == 0:
print 'no users'
if foo is not None and not foo:
self.handle_zero()
if not i % 10:
self.handle_multiple_of_ten()
# 注意‘0’(字符串)会被当做true.
14.函数与方法装饰器
如果好处很显然, 就明智而谨慎的使用装饰器
优点:优雅的在函数上指定一些转换. 该转换可能减少一些重复代码, 保持已有函数不变(enforce invariants), 等.
缺点:
- 装饰器可以在函数的参数或返回值上执行任何操作, 这可能导致让人惊异的隐藏行为.
- 而且, 装饰器在导入时执行.
- 从装饰器代码的失败中恢复更加不可能.
结论: 如果好处很显然, 就明智而谨慎的使用装饰器,但注意使用
- 装饰器应该遵守和函数一样的导入和命名规则.
- 装饰器的python文档应该清晰的说明该函数是一个装饰器.
- 请为装饰器编写单元测试.
- 避免装饰器自身对外界的依赖(即不要依赖于文件, socket, 数据库连接等), 因为装饰器运行时这些资源可能不可用(由
pydoc
或其它工具导入). - 应该保证一个用有效参数调用的装饰器在所有情况下都是成功的.
- 装饰器是一种特殊形式的”顶级代码”.
15.线程
优先使用Queue模块的
Queue
数据类型作为线程间的数据通信方式.
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。
这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
16.威力过大的特性
避免使用这些威力过大的特性
定义:
Python是一种异常灵活的语言, 它为你提供了很多花哨的特性, 诸如元类(metaclasses), 字节码访问, 任意编译(on-the-fly compilation), 动态继承, 对象父类重定义(object reparenting), 导入黑客(import hacks), 反射, 系统内修改(modification of system internals), 等等.
优点:
- 强大的语言特性, 能让你的代码更紧凑.
缺点:
- 使用这些很”酷”的特性十分诱人, 但不是绝对必要. 使用它们将更加难以阅读和调试.
- 开始可能还好, 但当你回顾代码, 它们可能会比那些稍长一点但是很直接的代码更加难以理解.
结论:莫装B
Google-Python编码规范 总结
在上面的内容中我们对Python中的Main的使用以及针对Python语言特性的语法规范进行了解读,那么希望小伙伴们通过本期《Google-Python编码规范》以及上一期《Python编程规范修炼-PEP8规范解读》的文章对Python编程的规范有了一个很深的认知,如果对本期关于编码规范的内容进行一个总结的话
请务必保持代码的一致性
如果你正在编辑代码, 花几分钟看一下周边代码, 然后决定风格. 比如如果它们在所有的算术操作符两边都使用空格, 那么你也应该这样做. 如果它们的注释都用标记包围起来, 那么你的注释也要这样.总之保持风格的统一才是王道