术语定义 规则: 编程时必须遵守的约定
说明: 某个规则的具体解释
建议: 编程时必须加以考虑的约定
错误示例: 违背某条规则的例子
正确示例: 遵循某条规则的例子
例外情况: 相应的规则不适用的场景
信任边界: 位于信任边界之内的所有组件都是被系统本身直接控制的。所有来自不受控的外部系统的连接与数据,包括客户端与第三方系统,都应该被认为是不可信的,要先在边界处对其校验,才能允许它们进一步与本系统交互。
非信任代码: 非产品包中的代码,如通过网络下载到本地虚拟机中加载并执行的代码。
- 排版 缩进 规则1.1 程序块采用4个空格缩进风格编写 说明:程序块采用缩进风格编写,缩进的空格数为4个,是业界通用的标准。
错误示例:空格个数不为4个
def load_data(dirname, one_hot=False): X_train = [] #5个空格 Y_train = [] #5个空格 Copy 正确示例:
def load_data(dirname, one_hot=False): X_train = [] Y_train = [] Copy 规则1.2 禁止混合使用空格(space)和跳格(Tab) 说明:推荐的缩进方式为仅使用空格(space)。仅使用跳格(Tab)也是允许的。如果已有代码中混合使用了空格及跳格,要全部转换为空格。
错误示例:空格和跳格混合使用
def load_data(dirname, one_hot=False): X_train = [] #跳格 Y_train = [] Copy 正确示例:
def load_data(dirname, one_hot=False): X_train = [] Y_train = [] Copy 规则1.3 新项目必须使用纯空格(spaces)来代替跳格(Tab) 说明:对于新项目,必须使用纯空格(spaces)来代替跳格(Tab)。
错误示例:新项目使用跳格
def load_data(dirname, one_hot=False): X_train = [] #跳格 Y_train = [] #跳格 Copy 正确示例:
def load_data(dirname, one_hot=False): X_train = [] Y_train = [] Copy 语句 规则1.4 Python文件中必须使用UTF-8编码 说明:Python文件中应该使用UTF-8编码(Python2.x中默认使用ASCII编码), 使用ASCII或UTF-8的文件必须有编码声明. 另外使用\x转义字符是在字符串中包含非ASCII(non-ASCII)数据的首选方法.
规则1.5 一行只写一条语句 说明:不允许把多个短语句写在一行中,即一行只写一条语句。多条语句写在一行,这样做一个很明显得缺点就是在调试的时候无法单步执行。
错误示例:多条语句在一行,不方便单步调试
rect.length = 0; rect.width = 0; Copy 正确示例:
rect.length = 0 rect.width = 0 Copy 规则1.6 相对独立的程序块之间、变量说明之后必须加空行 说明:相对独立的程序块之间、变量说明之后加上空行,代码可理解性会增强很多。
错误示例:程序块之间未加空行
if len(deviceName) < _MAX_NAME_LEN: …… writer = LogWriter() Copy 正确示例:
if len(deviceName) < _MAX_NAME_LEN: ……
writer = LogWriter() Copy 建议1.7 一行长度小于80个字符,与Python标准库看齐 说明:建议开发团队用本产品线的门禁工具或者yapf(github.com/google/yapf… 自动格式化,或者用IDE自带的格式化功能统一格式化代码后再提交。
较长的语句、表达式或参数(>80字符)要分成多行书写,首选使用括号(包括{},[],())内的行延续,推荐使用反斜杠(\)进行断行。长表达式要在低优先级操作符处划分新行,操作符统一放在新行行首或原行行尾,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
错误示例:一行字符太多,阅读代码不方便
if width == 0 and height == 0 and color == 'red' and emphasis == 'strong' and highlight > 100: x = 1 Copy 正确示例:
if width == 0
and height == 0
and color == 'red'
and emphasis == 'strong'
and highlight > 100:
x = 1
Copy
空格
规则1.8 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符前后要加空格
说明:采用这种松散方式编写代码的目的是使代码更加清晰。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留一个以上空格。
1、逗号、分号(假如用到的话)只在后面加空格。
错误示例:
print a,b , c Copy 正确示例:
print a, b, c Copy 2、比较操作符">"、">="、"<"、"=<"、"==", 赋值操作符"="、"+=",算术操作符"+"、"-"、"%",逻辑操作符and, or等双目操作符的前后加空格。
错误示例:
if current_time>= MAX_TIME_VALUE: a=b+ c a+=2 Copy 正确示例:
if current_time >= MAX_TIME_VALUE: a = b + c a += 2 Copy 建议1.9 进行非对等操作时,如果是关系密切的立即操作符(如 .),后不应加空格 函数定义语句中的参数默认值,调用函数传递参数时使用的等号,建议不加空格 def create(self, name=None) self.create(name=”mike”) Copy ""、"**"等作为操作符时,前后不加空格。 错误示例: a = b * c a = c ** b Copy 正确示例: a = bc a = c**b Copy "."前后不加空格。 错误示例: result. writeLog() Copy 正确示例: result.writeLog() Copy 括号内侧,左括号后面和右括号前面,不需要加空格,多重括号间不必加空格。 错误示例: a = ( (b + c)*d - 5 )*6 Copy 正确示例: a = ((b + c)*d - 5)*6 Copy 紧贴索引切片或被调用函数名,开始的括号前,不需要加空格。 错误示例: Dict [key] = list [index] conn = Telnet.connect (ipAddress) Copy 正确示例: dict[key] = list[index] conn = Telnet.connect(ipAddress) Copy 导入 规则1.10 加载模块必须分开每个模块占一行 说明:单独使用一行来加载模块,让程序依赖变得更清晰。
错误示例:
import sys, os Copy 正确示例:
import sys import os Copy 规则1.11 导入部分(imports)置于模块注释和文档字符串之后,模块全局变量和常量声明之前 说明:导入部分(imports)置于模块注释和文档字符串之后,模块全局变量和常量声明之前。导入(import)库时,按照标准库、第三方关联库、本地特定的库/程序顺序导入,并在这几组导入语句之间增加一个空行。
正确示例:
import sys import os
from oslo_config import cfg from oslo_log import log as logging
from cinder import context from cinder import db Copy 建议1.12 避免使用from xxx import *的方式导入某模块的所有成员。 说明: from xxx import *会将其他模块中的所有成员挨个赋值给当前范围的同名变量,如果当前范围已经有同名变量,则会静默将其覆盖。这种方式容易导致名字冲突,且冲突后不容易定位,应当尽量避免使用。
正确示例: 如果需要使用yyy,则from xxx import yyy
解释器 建议1.13 类Unix操作系统上直接执行的Python文件头部建议使用#!/usr/bin/env python指定解释器 说明:类Unix操作系统上使用Hashbang “#!/usr/bin/env python”声明的时候,会去取系统的 PATH 变量中指定的第一个 Python 来执行你的脚本,有助于正确指定执行Python文件的解释器。Hashbang的位置需要放在文件编码声明之前。 Windows操作系统可忽略此建议。
- 注释 类、接口和函数 规则2.1 类和接口的注释写在类声明(class ClassName:)所在行的下一行,并向后缩进4个空格 说明:功能描述除了描述类或接口功能外,还要写明与其他类或接口之间的关系;属性清单列出该类或接口的接口方法的描述;修改记录包括修改人,修改日期及修改内容。
正确示例:
class TreeError(libxmlError): """ 功能描述: 接口: 修改记录: """ Copy 规则2.2 公共函数的注释写在函数声明(def FunctionName(self):)所在行的下一行,并向后缩进4个空格 说明:公共函数注释的内容包括功能描述、输入参数、输出参数、返回值、调用关系(函数、表)、异常描述,修改记录等。异常描述必须说明异常的含义及什么条件下抛出该异常,除描述函数内部抛出的异常外。
正确示例:
def load_batch(fpath): """ 功能描述: 参数: 返回值: 异常描述: 修改记录 """ Copy 属性 规则2.3 公共属性的注释写在属性声明的上方,与声明保持同样的缩进。行内注释应以#和一个空格作为开始,与后面的文字注释以一个空格隔开 说明:行内注释的形式是在语句的上一行中加注释。行内注释要少用。它们应以#和一个空格作为开始。
错误示例:
#Compensate for border x = x + 1 Copy 正确示例:
Compensate for border
x = x + 1 Copy 格式 规则2.4 模块注释写在文件的顶部,导入(import)部分之前的位置,不需要缩进 说明:每次模块代码修改后要写明修改信息,修改信息包括修改人,修改日期及修改内容。
正确示例:
""" 功 能:XXX类,该类主要涉及XXX功能
修改记录:2015-3-17 12:00 XXX XXXXXXXX 创建 2017-3-17 12:00 XXX XXXXXXXX 修改 XXX """ Copy 规则2.5 文档字符串多于一行时,末尾的"""要自成一行 说明:对于只有一行的文档字符串,把"""放到同一行也没问题。单行可以放同一行。
错误示例:
"""Return a foobang Optional plotz says to frobnicate the bizbaz first.""" Copy 正确示例:
"""Return a foobang Optional plotz says to frobnicate the bizbaz first. """ Copy 正确示例:
"""API for interacting with the volume manager.""" Copy 规则2.6 注释必须与其描述的代码保持同样的缩进,并放在其上方相邻位置 说明:注释应与其描述的代码相近,并与所描述的代码保持同样的缩进。对代码的注释应放在其上方相邻位置,不可放在下面。
错误示例:注释与所描述的代码有不同的缩进
# get replicate sub system index and net indicator
repssn_ind = ssn_data[index].repssn_index repssn_ni = ssn_data[index].ni Copy 正确示例:
get replicate sub system index and net indicator
repssn_ind = ssn_data[index].repssn_index repssn_ni = ssn_data[index].ni Copy 正确示例:
if image_service is not None: # Deletes the image if it is in queued or saving state self._delete_image(context, image_meta['id'], image_service) Copy 建议 建议2.7 源程序有效注释量(包括DocString)应该在20%以上 说明:注释的原则是有助于对程序的阅读理解,没有类型信息,IDE不能帮助提示,如果没有注释,动态语言就很难理解,注释不宜太多也不能太少,注释描述必须准确、易懂、简洁。
建议2.8 注释的内容要清楚,防止注释二义性 说明:错误的注释不但无益反而有害。注释的要点是准确,没有二义性。把代码说清楚是目的。
建议2.9 避免在注释中使用缩写 说明:在使用缩写时或之前,应对缩写进行必要的说明。
建议2.10 保持代码和注释的同步修改 说明:边写代码边注释,修改代码时始终优先更新相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
建议2.11 有含义的变量应该加上注释 说明:对于有物理含义的变量,如果其命名不是充分自注释的,在声明时必须加以注释,说明其物理含义。变量的注释应放在其上方相邻位置。
错误示例:
没有注释
_MAX_ACT_TASK_NUMBER = 1000 Copy 正确示例:
maximum number of active statistic tasks
_MAX_ACT_TASK_NUMBER = 1000 Copy 建议2.12 全局变量要有较详细的注释 说明:全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程修改它以及存取时注意事项等的说明。
3.命名 包和模块 规则3.1 包(Package)、模块(Module)名使用意义完整的英文描述,采用小写加下划线(lower_with_under)的风格命名 说明:模块应该用小写加下划线的方式(如lower_with_under.py)命名。尽管已经有很多现存的模块使用类似于CapWords.py这样的命名,但现在已经不鼓励这样做, 因为如果模块名碰巧和类名一致, 这会让人困扰。
正确示例:
from sample_package import sample_module from sample_module import SampleClass Copy 类 规则3.2 类(Class)名使用意义完整的英文描述,采用大写字母开头的单词(CapWords)风格命名 说明:类沿用面向对象语言最常用的CapWords风格命名。
正确示例:
class SampleClass(object): pass Copy 函数 规则3.3 函数(Function)、方法(Method)、函数参数(Function Parameters)名使用意义完整的英文描述,采用小写加下划线(lower_with_under)的风格命名 说明: 函数、方法采用小写加下划线的风格命名,与类名做区分。 函数参数采用小写加下划线的风格命名,与一般变量的命名风格保持一致。 模块内部使用的函数用单下划线(_)开头,表示函数是protected的(使用from module1 import *时不会包含)。
正确示例:
def sample_public_function(sample_parameter): pass
def sample_internal_function(sample_parameter): pass
class SampleClass(object):
def sample_member_method(self, sample_parameter):
pass
Copy 变量 规则3.4 变量(variable)采用小写加下划线(lower_with_under)的风格命名。常量(constant)采用大写加下划线(CAPS_WITH_UNDER)的风格命名 说明:
常量使用大写加下划线的风格命名,与变量做区分。
正确示例:
sample_global_variable = 0 M_SAMPLE_GLOBAL_CONSTANT = 0
class SampleClass(object):
SAMPLE_CLASS_CONSTANT = 0
def sample_member_methond(self, sample_parameter):
pass
def sample_function(): sample_function_variable = 0 sample_instant_variable = SampleClass() Copy 规则3.5 类或对象的私有成员一般用单下划线_开头;对于需要被继承的基类成员,如果想要防止与派生类成员重名,可用双下划线__开头。 说明: Python没有严格的私有权限控制,业界约定俗成的用单下划线“_”开头来暗示此成员仅供内部使用。双下划线“__”开头的成员会被解释器自动改名,加上类名作为前缀,其作用是防止在类继承场景中出现名字冲突,并不具有权限控制的作用,外部仍然可以访问。双下划线开头的成员应当只在需要避免名字冲突的场景中使用(比如设计为被继承的工具基类)。
正确示例:
class MyClass: def my_func(self): self._member = 1 # 单下划线开头,暗示此成员仅供类的内部操作使用,外部不应该访问。
def _my_private_func(self): # 单下划线开头,暗示此方法仅供类的内部操作使用,外部不应该访问。
pass
class Mapping: def init(self, iterable): self.items_list = [] self.__update(iterable) # 双下划线开头,会被解释器改名为_Mapping__update。外部如果使用修改后的名字仍可访问
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # 作为update方法的私有复制成员,不会跟派生类成员重名
class MappingSubclass(Mapping): # 和基类同名方法,修改了参数个数,但是不会影响基类__init__ def update(self, keys, values): for item in zip(keys, values): self.items_list.append(item)
__update = update # 被解释器改名为_MappingSubclass__update,不会跟基类成员重名
Copy 参考资料:docs.python.org/3/tutorial/…
建议3.6 变量(variable)命名要有明确含义,使用完整的单词或大家基本可以理解的缩写,避免使人产生误解 说明:
命名中若使用了特殊约定或缩写,建议注释说明。 对于变量命名,除局部循环变量之外,不允许取单个字符(如i、j、k)。 不要用单个字符"l","o"来做变量名称。在有些字体中,这些字符于数字很难1和0很难辨认。若确实需要使用"l"做变量,用"L"来替换。 错误示例:
class SampleClass(object): pass
def sample_function(sample_parameter): i = SampleClass() o = [l for l in range(1)] Copy 正确示例:
class SampleClass(object): pass
def sample_function(sample_parameter): sample_inst = SampleClass() number_list = [i for i in range(10)] Copy 命名规范推荐表 建议3.7 命名规范推荐表 Python之父Guido推荐的命名规范
Type Public Internal Modules lower_with_under _lower_with_under Packages lower_with_under Classes CapWords Exceptions CapWords Functions lower_with_under() _lower_with_under() Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER Global/Class Variables lower_with_under lower_with_under Instance Variables lower_with_under lower_with_under (protected) or __lower_with_under (private) Method Names lower_with_under() _lower_with_under() (protected) or __lower_with_under() (private) Function/Method Parameters lower_with_under Local Variables lower_with_under 4. 编码 规则4.1 与None作比较要使用“is”或“is not”,不要使用等号 说明:
“is”判断是否指向同一个对象(判断两个对象的id是否相等),“==”会调用eq方法判断是否等价(判断两个对象的值是否相等)。
示例:
同一个实例,使用“is”和“==”的判断结果不同。
class Bad(object): def eq(self, other): return True bad_inst = Bad() bad_inst == None True bad_inst is None False Copy 建议4.2 定义一个all不会把本模块的所有内容都暴露在外部,将允许外部访问的变量、函数和类的名字放进去 说明:
在模块中定义了__all__之后,从外部from module import *只会import __all__中定义的内容。
示例:
sample_package.py
all = ["sample_external_function"]
def sample_external_function(): print("This is an external function..")
def sample_internal_function(): print("This is an internal function..") Copy main.py
from sample_package import *
if name == "main": sample_external_function() sample_internal_function()
NameError: name 'sample_internal_function' is not defined Copy 建议4.3 避免直接使用dict[key]的方式从字典中获取value,如果一定要使用,需要注意当key not in dict时的异常捕获和处理 说明:
Python的字典dict可以使用key获取其对应的value。但是当key在dict的key值列表中不存在时,直接使用dict[key]获取value会报KeyError,应当使用更为安全的dict.get(key)类型方法获取value。
错误示例:
sample_dict = {'default_key': 1} sample_key = 'sample_key' sample_value = sample_dict[sample_key] Copy 正确示例:
sample_dict = {'default_key': 1} sample_key = 'sample_key' sample_value = sample_dict.get(sample_key) Copy 建议4.4 对序列使用切片操作时,不建议使用负步进值进行切片 说明:
Python提供了sample_list[start : end : stride]形式的写法,以实现步进切割,也就是从每stride个元素中取一个出来。但如果stride值为负,则会使代码难以理解,特定使用场景下还会造成错误。
错误示例:
如下写法,在start : end : stride都使用的情况下使用负的stride,会造成阅读困难。此种情况建议将“步进”切割过程和“范围”切割过程分开,使代码更清晰。
a = [1,2,3,4,5,6,7,8] a[2::2] [3,5,7] a[-2::-2] [7,5,3,1] a[-2:2:-2] [7,5] a[2:2:-2] [] Copy 建议4.5 传递实例类型参数后,函数内应使用isinstance函数进行参数检查,不要使用type 说明:如果类型有对应的工厂函数,可使用它对类型做相应转换,否则可以使用isinstance函数来检测。使用函数/方法参数传递实例类型参数后,函数内对此参数进行检查应使用isinstance函数,使用is not None,len(para) != 0等其它逻辑方法都是不安全的。
错误示例:
下面的函数保护未能完成应有的检查功能,传入一个tuple就可以轻易绕过保护代码造成执行异常。
def sample_sort_list(sample_inst): ... if sample_inst is []: ... return ... sample_inst.sort() fake_list = (2,3,1,4) sample_sort_list(fake_list) Traceback (most recent call last): File "<pyshell#232>", line 1, in sample_sort_list(fake_list) File "<pyshell#230>", line 4, in sample_sort_list sample_inst.sort() AttributeError: 'tuple' object has no attribute 'sort' Copy 正确示例:
使用instance函数对入参进行检查,检查后可以按照需求raise exception或return。
def sample_sort_list(sample_inst): ... if not isinstance(sample_inst, list): ... raise TypeError(r"sample_sort_list in para type error %s" % type(sample_inst)) ... sample_inst.sort() fake_list = (2,3,1,4) sample_sort_list(fake_list) Traceback (most recent call last): File "<pyshell#235>", line 1, in sample_sort_list(fake_list) File "<pyshell#234>", line 3, in sample_sort_list raise TypeError(r"sample_sort_list in para type error %s" % type(sample_inst)) TypeError: sample_sort_list in para type error <type 'tuple'> Copy 建议4.6 尽量使用推导式代替重复的逻辑操作构造序列。但推导式必须考虑可读性,不推荐使用两个以上表达式的列表推导 说明:
推导式(comprehension)是一种精炼的序列生成写法,在可以使用推导式完成简单逻辑,生成序列的场合尽量使用推导式,但如果逻辑较为复杂(> 两个逻辑表达式),则不推荐强行使用推导式,因为这会使推导式代码的可读性变差。
错误示例:
如下逻辑代码,逻辑较为简单,实现此逻辑的代码不但有循环,而且较为复杂,性能不佳。
odd_num_list = [] for i in range(100): if i % 2 == 1: odd_num_list.append(i) Copy 正确示例:
odd_num_list = [i for i in range(100) if i % 2 == 1] Copy 简单逻辑使用列表推导式实现,代码清晰精炼。
建议4.7 功能代码应该封装在函数或类中 说明:在Python中, 所有的顶级代码在模块导入时都会被执行. 容易产生调用函数, 创建对象等误操作。所以代码应该封装在函数或类中。即使是脚本类的代码,也建议在执行主程序前总是检查 if name == 'main' , 这样当模块被导入时主程序就不会被执行.
正确示例:
def main(): ...
if name == 'main': main() Copy 建议4.8 需要精确数值计算的场景,应使用decimal模块 说明:在Python中,注意不要用浮点数构造Decimal,因为浮点数本身不准确。
正确示例:
from decimal import Decimal Decimal('3.14') Decimal('3.14') getcontext().prec = 6 Decimal(1) / Decimal(7) Decimal('0.142857') Copy 错误示例:
from decimal import Decimal getcontext().prec = 28 Decimal(3.14) Decimal('3.140000000000000124344978758017532527446746826171875') Copy 规则4.9 避免对不同对象使用同一个变量名 说明: Python是弱类型语言,允许变量被赋值为不同类型对象,但这么做可能会导致运行时错误,且因为变量上下文语义变化导致代码复杂度提升,难以调试和维护,也不会有任何性能的提升。
错误示例
items = 'a,b,c,d' # 字符串 items = items.split(',') # 变更为列表 Copy 正确示例
items = 'a,b,c,d' # 字符串 itemList = items.split(',') # 变更为列表 Copy 规则4.10 类的方法不需访问实例时,根据具体场景选择使用@staticmethod或者@classmethod进行装饰 说明: 一般的类方法要接收一个self参数表示此类的实例,但有些方法不需要访问实例,这时分为两种情况: 1、方法不需要访问任何成员,或者只需要显式访问这个类自己的成员。这样的方法不需要额外参数,应当用@staticmethod装饰。 在Python 3.X版本中,允许直接定义不含self参数的方法,并且允许不通过实例调用。但是一旦通过实例调用这个方法,就会因为参数不匹配而出错。 加上@staticmethod进行修饰,可以让Python解释器明确此方法不需要self参数,提前拦截问题,可读性也更好。
错误示例:
class MyClass: def my_func(): # 没有用@staticmethod修饰,通过实例调用会出错 pass
MyClass.my_func() # Python 3.X中允许,2.X中出错 my_instance = MyClass() my_instance.my_func() # Python 3.X和2.X中都会出错 Copy 正确示例:
class MyClass: @staticmethod def my_func(): # 用@staticmethod修饰后,解释器会将其解析为静态方法 pass
MyClass.my_func() # OK my_instance = MyClass() my_instance.my_func() # OK,但是不推荐,容易和普通方法混淆。最好写成MyClass.my_func() Copy 2、方法不需要访问实例的成员,但需要访问基类或派生类的成员。这时应当用@classmethod装饰。装饰后的方法,其第一个参数不再传入实例,而是传入调用者的最底层类。 比如,下面这个例子,通过基类Spam的count方法,来统计继承树上每个类的实例个数:
class Spam: numInstances = 0 @classmethod def count(cls): # 对每个类做独立计数 cls.numInstances += 1 # cls是实例所属于的最底层类 def init(self): self.count() # 将self.__class__传给count方法
class Sub(Spam): numInstances = 0
class Other(Spam): numInstances = 0
x = Spam() y1, y2 = Sub(), Sub() z1, z2, z3 = Other(), Other(), Other() x.numInstances, y1.numInstances, z1.numInstances # 输出:(1, 2, 3) Spam.numInstances, Sub.numInstances, Other.numInstances # 输出:(1, 2, 3) Copy 但是使用@classmethod时需要注意,由于在继承场景下传入的第一个参数并不一定是这个类本身,因此并非所有访问类成员的场景都应该用@classmethod。比如下面这个例子中,Base显式的想要修改自己的成员inited(而不是派生类的成员),这时应当用@staticmethod。
错误示例:
class Base: inited = False @classmethod def set_inited(cls): # 实际可能传入Derived类 cls.inited = True # 并没有修改Base.inited,而是给Derived添加了成员
class Derived(Base): pass
x = Derived() x.set_inited() if Base.inited: print("Base is inited") # 不会被执行 Copy 建议4.11 当多个Python源码文件分不同子目录存放时,用包(package)形式管理各个目录下的模块。 说明: 通过让子目录包含__init__.py文件,可以让Python代码在import和from语句中,将子目录作为包名,通过分层来管理各个模块,让模块间的关系更清楚。init.py文件中可以包含这个包所需要的初始化动作,也可以定义一个__all__列表来指定from *语句会包含哪些模块。对于不需要初始化的包,可以只在目录下放一个名为__init__.py的空文件,标识这个目录是一个包。
正确示例: 假设Python源码根目录是dir0,其下有子目录dir1,dir1下面又有个子目录dir2,dir2下面有个mod.py模块。 那么,在dir1和dir2下各放置一个__init__.py文件,然后在其他代码中可以这样使用mod.py模块:
import dir1.dir2.mod dir1.dir2.mod.func() # 调用mod.py中的func函数
from dir1.dir2.mod import func # 把func函数添加到当前空间 func() # 可以省掉包名和模块名直接调用 Copy 建议4.12 避免在代码中修改sys.path列表 说明: sys.path是Python解释器在执行import和from语句时参考的模块搜索路径,由当前目录、系统环境变量、库目录、.pth文件配置组合拼装而成。用户通过修改系统配置,可以指定搜索哪个路径下的模块。sys.path只应该根据用户的系统配置来生成,不应该在代码里面直接修改。否则可能出现A模块修改了sys.path,导致B模块搜索出错,且用户难以定位。
正确示例: 如果要添加模块搜索路径,应当修改PYTHONPATH环境变量。如果是管理子目录,应当通过包(package)来组织模块。
建议4.13 尽量不使用for i in range(x)的方式循环处理集合数据,而应使用for x in iterable的方式 说明: for i in range(x),然后在循环体内对集合用下标[i]获取元素是C语言的编程习惯,它有很多缺点:容易越界;在循环体内修改i容易出错;可读性差。Python语言建议尽量用for x in iterable的方式直接取集合的每一条数据进行处理。
错误示例:
for i in range(len(my_list)): print(my_list[i]) Copy 正确示例:
for x in my_list: print(x) Copy 有些场合下,需要在处理时使用每个元素的序号,这时可以使用enumerate内置函数来给元素加上序号形成元组:
my_list = ['a', 'b', 'c'] for x in enumerate(my_list): print(x) Copy 运行结果为: (0, 'a') (1, 'b') (2, 'c')
规则4.14 避免在无关的变量或无关的概念之间重用名字,避免因重名而导致的意外赋值和错误引用 说明: Python的函数/类定义和C语言不同,函数/类定义语句实际上是给一个名字赋值。因此重复定义一个函数/类的名字不会导致错误,后定义的会覆盖前面的。但是重复定义很容易掩盖编码问题,让同一个名字的函数/类在不同的执行阶段具有不同的含义,不利于可读性,应予以禁止。 Python在解析一个被引用的名字时遵循LEGB顺序(Local - Enclosed - Global - Builtin),从内层一直查找到外层。内层定义的变量会覆盖外层的同名变量。在代码修改时,同名的变量容易导致错误的引用,也不利于代码可读性,应当尽量避免。
- 异常处理 异常处理 规则5.1 使用try…except…结构对代码作保护时,需要在异常后使用finally…结构保证操作对象的释放 说明:
使用try…except…结构对代码作保护时,如果代码执行出现了异常,为了能够可靠地关闭操作对象,需要使用finally…结构确保释放操作对象。
示例:
handle = open(r"/tmp/sample_data.txt") # May raise IOError try: data = handle.read() # May raise UnicodeDecodeError except UnicodeDecodeError as decode_error: print(decode_error) finally: handle.close() # Always run after try: Copy 规则5.2 不要使用“except:”语句来捕获所有异常 说明:
在异常这方面, Python非常宽容,“except:”语句真的会捕获包括Python语法错误在内的任何错误。使用“except:”很容易隐藏真正的bug,我们在使用try…except…结构对代码作保护时,应该明确期望处理的异常。 Exception类是大多数运行时异常的基类,一般也应当避免在except语句中使用。通常,try只应当包含必须要在当前位置处理异常的语句,except只捕获必须处理的异常。比如对于打开文件的代码,try应当只包含open语句,except只捕获FileNotFoundError异常。对于其他预料外的异常,则让上层函数捕获,或者透传到程序外部来充分暴露问题。
错误示例:
如下代码可能抛出两种异常,使用“except:”语句进行统一处理时,如果是open执行异常,将在“except:”语句之后handle无效的情况下调用close,报错handle未定义。
try: handle = open(r"/tmp/sample_data.txt") # May raise IOError data = handle.read() # May raise UnicodeDecodeError except: handle.close() Copy 正确示例:
try: handle = open(r"/tmp/sample_data.txt") # May raise IOError try: data = handle.read() # May raise UnicodeDecodeError except UnicodeDecodeError as decode_error: print(decode_error) finally: handle.close()
except(FileNotFoundError, IOError) as file_open_except: print(file_open_except) Copy 规则5.3 不在except分支里面的raise都必须带异常 说明:raise关键字单独使用只能出现在try-except语句中,重新抛出except抓住的异常。
错误示例:
a = 1 if a==1: ... raise ... Traceback (most recent call last): File "", line 2, in TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType Copy 正确示例1:raise一个Exception或自定义的Exception
a = 1 if a==1: ... raise Exception ... Traceback (most recent call last): File "", line 2, in Exception Copy 正确示例2:在try-except语句中使用
import sys
try: ... f = open('myfile.txt') ... s = f.readline() ... i = int(s.strip()) ... except IOError as e: ... print "I/O error({0}): {1}".format(e.errno, e.strerror) ... except ValueError: ... print "Could not convert data to an integer." ... except: ... print "Unexpected error:", sys.exc_info()[0] ... raise Copy 建议5.4 尽量用异常来表示特殊情况,而不要返回None 当我们在一个工具方法时,通常会返回None来表明特殊的意义,比如一个数除以另外一个数,如果被除数为零,那么就返回None来表明是没有结果的
def divide(a, b): try: return a/b except ZeroDivisionError: return None
result = divide(x, y) if result is None: print('Invalid inputs') Copy 当分子为零是会返回什么?应该是零(如果分母不为零的话),上面的代码在if条件检查就会被忽略掉,if条件不仅仅只检查值为None,还要添加所有条件为False的情况了
x, y = 0, 5 result = divide(x, y) if not result: print('Invalid inputs') #This is wrong! Copy 上面的情况是python编码过程中很常见,这也为什么方法的返回值为None是一种不可取的方式,这里有两种方法来避免上面的错误。
1.第一种方法是将返回值分割成一个tuple,第一部分表示操作是否成功,第二部分是实际的返回值(有点象go语言里的处理)
def divide(a, b): try: return True, a / b except ZeroDivisionError: return False, None Copy 调用此方法时获取返回值并解开,检查第一部分来代替之前仅仅检查结果。
success, result = divide(x, y) if not success: print('Invalid inputs') Copy 这种方式会带来另外一个问题,方法的调用者很容易忽略掉tuple的第一部分(通过在python里可以使用_来标识不使用的变量),这样的代码乍一看起来不错,但是实际上和直接返回None没什么两样。
_, result = divide(x, y) if not result: print('Invalid inputs') Copy 2.接下来,另外一种方式,也是推荐的一种方式,就是触发异常来让调用者来处理,方法将触发ValueError来包装现有的ZeroDivisionError错误用来告诉方法调用者输入的参数是有误的。
def divide(a, b): try: return a / b except ZeroDivisionError as e: raise ValueError('Invalid inputs') from e Copy 那么方法调用者必须要处理错误输入值而产生的异常(方法的注释应该注明异常情况)。同时也不用去检查返回值,因为当方法没有异常抛出时,返回值一定是对的,对于异常的处理也是很清晰。
x, y = 5, 2 try: result = divide(x, y) except ValueError: print('Invalid inputs') else: print('Result is %.1f' % result)
Result is 2.5 Copy 需要记住的: (1)方法使用None作为特殊含义做为返回值是非常糟糕的编码方式,因为None和其它的返回值必须要添加额外的检查代码。 (2)触发异常来标示特殊情况,调用者会在捕获异常来处理。
建议5.5 避免finally中可能发生的陷阱,不要在finally中使用return或者break语句 通常使用finally语句,表明要释放一些资源,这时候try和except还有else代码块都被执行过了,如果在执行它们的过程中有异常触发,且没有处理这个异常,那么异常会被暂存,当finally代码执行后,异常会重新触发,但是当finally代码块里有return或break语句时,这个暂存的异常就会丢弃:
def f(): try: 1/0 finally: return 42
print(f()) Copy 上面的代码执行完后1/0产生的异常就会被忽略,最终输出42,因此在finally里出现return是不可取的。
当try块中return,break,continue执行时,finally块依然会被执行。
def foo(): try: return 'try' finally: return 'finally'
foo() 'finally' Copy 最终方法的输出其实不是正确的结果,但出现这个问题的原因是错误使用了return和break语句。
规则5.6 禁止使用except X, x语法,应当使用except X as x 说明: except X, x语法只在2.X版本支持,3.X版本不支持,有兼容性问题。而且,except X, x写法容易和多异常捕获的元组(tuple)表达式混淆。因此应该统一用except X as x方式。
异常恢复 建议5.7 方法发生异常时要恢复到之前的对象状态 说明:当发生异常的时候,对象一般需要——如果是关键的安全对象则必须——维持其状态的一致性。常用的可用来维持对象状态一致性的手段包括:
输入校验(如校验方法的调用参数) 调整逻辑顺序,使可能发生异常的代码在对象被修改之前执行 当业务操作失败时,进行回滚 对一个临时的副本对象进行所需的操作,直到成功完成这些操作后,才把更新提交到原始的对象 避免需要去改变对象状态 错误示例:
PADDING = 2 MAX_DIMENSION = 10
class Dimensions: def init(self, length, width, height): self.length = length self.width = width self.height = height
def get_volume_package(self, weight):
self.length += PADDING
self.width += PADDING
self.height += PADDING
try:
self.validate(weight)
volume = self.length * self.width * self.height
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
return volume
except Exception , ex:
return -1
def validate(self, weight) :
# do some validation and may throw a exception
if weight>20:
raise Exception
pass
if name == 'main' d = Dimensions(10, 10, 10) print d.getVolumePackage(21) # Prints -1 (error) print d.getVolumePackage(19) # Prints 2744 instead of 1728 Copy 在这个错误示例中,未有异常发生时,代码逻辑会恢复对象的原始状态。但是如果出现异常事件,则回滚代码不会被执行,从而导致后续的getVolumePackage()调用不会返回正确的结果。
正确示例1:回滚
except Exception , ex: self.length -= PADDING self.width -= PADDING self.height -= PADDING return -1 Copy 这个正确示例在getVolumePackage()方法的except块中加入了发生异常时恢复对象状态的代码。
正确示例2:finally子句
def getVolumePackage(self, weight): self.length += PADDING self.width += PADDING self.height += PADDING try: self.validate(weight) volume = self.length * self.width * self.height return volume except Exception , ex: return -1 finally: self.length -= PADDING self.width -= PADDING self.height -= PADDING Copy 这个正确示例使用一个finally子句来执行回滚操作,以保证不管是否发生异常,都会进行回滚。
正确示例3:输入校验
def getVolumePackage(self, weight): try: self.validate(weight) except Exception , ex: return -1
self.length += PADDING
self.width += PADDING
self.height += PADDING
volume = self.length * self.width * self.height
self.length -= PADDING
self.width -= PADDING
self.height -= PADDING
return volume
Copy 这个正确示例在修改对象状态之前执行输入校验。注意,try代码块中只包含可能会抛出异常的代码,而其他代码都被移到try块之外。
正确示例4:未修改的对象
def getVolumePackage(self, weight): try: self.validate(weight) except Exception , ex: return -1
self.length += PADDING
self.width += PADDING
self.height += PADDING
volume = (self.length + PADDING) * (self.width + PADDING) * (self.height + PADDING)
return volume
Copy 这个正确示例避免了需要修改对象,使得对象状态不可能不一致,也因此没有必要进行回滚操作。相比之前的解决方案,更推荐使用这种方式。但是对于一些复杂的代码,这种方式可能无法实行。
断言 建议5.8 assert语句通常只在测试代码中使用,禁止在生产版本中包含assert功能 assert语句用来声明某个条件是真的。例如,如果你非常确信某个列表中至少有一个元素,而你想检验这一点,并且在它非真时触发一个异常,那么assert就是这种场景下的不二之选。当assert语句失败的时候,会触发AssertionError异常
mylist = ['item'] assert len(mylist) >= 1 mylist.pop() 'item' assert len(mylist) >= 1 Traceback (most recent call last): File "", line 1, in ? AssertionError Copy assert只应在研发过程中内部测试时使用,出现了AssertionError异常说明存在软件设计或者编码上的错误,应当修改软件予以解决。在对外发布的生产版本中禁止包含assert功能。
- 并发与并行 线程 建议6.1 多线程适用于阻塞式IO场景,不适用于并行计算场景 Python的标准实现是CPython。
CPython执行Python代码分为2个步骤:首先,将文本源码解释编译为字节码,然后再用一个解释器去解释运行字节码。字节码解释器是有状态的,需要维护该状态的一致性,因此使用了GIL(Global Interpreter Lock,全局解释器锁)。
GIL的存在,使得CPython在执行多线程代码的时候,同一时刻只有一个线程在运行,无法利用多CPU提高运算效率。但是这个特点也带来了一个好处:CPython运行多线程的时候,内部对象缺省就是线程安全的。这个特性,被非常多的Python库开发者所依赖,直到CPython的开发者想要去除GIL的时候,发现已经有大量的代码库重度依赖这个GIL带来的内部对象缺省就是线程安全的特性,变成一个无法解决的问题了。
虽然多线程在并行计算场景下无法带来好处,但是在阻塞式IO场景下,却仍然可以起到提高效率的作用。这是因为阻塞式IO场景下,线程在执行IO操作时并不需要占用CPU时间,此时阻塞IO的线程可以被挂起的同时继续执行IO操作,而让出CPU时间给其他线程执行非IO操作。这样一来,多线程并行IO操作就可以起到提高运行效率的作用了。
综上,Python的标准实现CPython,由于GIL的存在,同一个时刻只能运行一个线程,无法充分利用多CPU提升运算效率,因此Python的多线程适用于阻塞式IO的场景,不适用于并行计算的场景。
下面举一个对计算量有要求的求一个数的因数分解的代码实例,来说明Python多线程不适用于并行计算的场景:
-- coding:utf-8 --
from time import time from threading import Thread
def factorize(number): for i in range(1, number + 1): if number % i == 0: yield i
class FactorizeThread(Thread): def init(self, number): Thread.init(self) self.number = number
def run(self):
self.factors = list(factorize(self.number))
def test(numbers): start = time() for number in numbers: list(factorize(number)) end = time() print('Took %.3f seconds' % (end - start))
def test_thread(numbers): start = time() threads = [] for number in numbers: thread = FactorizeThread(number) thread.start() threads.append(thread) for t in threads: t.join() end = time() print('Mutilthread Took %.3f seconds' % (end - start))
if name == "main": numbers = [2139079, 1214759, 1516637, 1852285]
test(numbers)
test_thread(numbers)
Copy 代码输出:
Took 0.319 seconds Mutilthread Took 0.539 seconds Copy 以上代码运行结果只是一个参考值,具体数据跟运行环境相关。但是可以看到单线程方式比多线程方式的计算速度要快。由于CPython运行多线程代码时因为GIL的原因导致每个时刻只有一个线程在运行,因此多线程并行计算并不能带来时间上的收益,反而因为调度线程而导致总时间花费更长。
对于IO阻塞式场景,多线程的作用在于发生IO阻塞操作时可以调度其他线程执行非IO操作,因此在这个场景下,多线程是可以节省时间的。可以用以下的代码来验证:
-- coding:utf-8 --
from time import time from threading import Thread import os
def slow_systemcall(n): for x in range(100): open("test_%s" % n, "a").write(os.urandom(10) * 100000)
def test_io(N): start = time() for _ in range(N): slow_systemcall(_) end = time() print('Took %.3f seconds' % (end - start))
def test_io_thread(N): start = time() threads = [] for _ in range(N): thread = Thread(target=slow_systemcall, args=("t_%s"%_,)) thread.start() threads.append(thread) for thread in threads: thread.join() end = time() print('Took %.3f seconds' % (end - start))
if name == "main": N = 5 test_io(N) test_io_thread(N) Copy 代码输出:
Took 5.179 seconds Multithread Took 1.451 seconds Copy 可以看到单线程花费时间与多线程花费时间之比接近1:4,考虑线程调度的时间,这个跟一般语言的多线程起的作用比较相似。这是因为当Python执行IO操作时,实际上是执行了系统调用,此时线程会释放GIL,直到系统调用结束时,再申请获取GIL,也就是在IO操作期间,线程确实是并行执行的。
Python的另外一个实现JPython就没有GIL,但是它并不是最常见的Python实现。
建议6.2 建议使用Queue来协调各线程之间的工作 如果Python程序同时要执行许多事务,那么开发者经常需要协调这些事务。而在各种协调方式中,较为高效的一种,则是采用函数管线。
管线的工作原理,与制造业中的组装生产线(assembly line)相似。管线分为许多首尾相连的阶段,每个阶段都由一种具体的函数来负责。程序总是把待处理的新部件添加到管线的开端。每一种函数都可以在它所负责的那个阶段内,并发地处理位于该阶段的部件。等负责本阶段的那个函数把某个部件处理好之后,该部件就会传送到管线中的下一个阶段,以此类推,直到全部阶段都经历一遍。涉及阻塞式I/O操作或子进程的工作任务,尤其适合用此办法处理,这样的任务很容易分配到多个Python线程或进程中。
例如,要构建一个照片处理系统,该系统从数码相机里面持续获取照片、调整其尺寸,并将其添加到网络相册中。这样的程序,可以采用三个阶段的管线来做。第一个阶段获取新图片,第二个阶段把下载好的图片传给缩放函数,第三个阶段把缩放后的图片交给上传函数。利用内置模块Queue中的Queue类来实现,可以变得容易且健壮。示例代码如下:
from Queue import Queue from threading import Thread
def download(item): print 'download item' return item
def resize(item): print 'resize item' return item
def upload(item): print 'upload item' return item
class ClosableQueue(Queue): SENTINEL = object()
def close(self):
self.put(self.SENTINEL)
def __iter__(self):
while True:
item = self.get()
try:
if item is self.SENTINEL:
return # Cause the thread to exit
yield item
finally:
self.task_done()
class StoppableWorker(Thread): def init(self, func, in_queue, out_queue): super(StoppableWorker, self).init() self.in_queue = in_queue self.out_queue = out_queue self.func = func
def run(self):
for item in self.in_queue:
result = self.func(item)
self.out_queue.put(result)
if name == 'main': download_queue = ClosableQueue() resize_queue = ClosableQueue() upload_queue = ClosableQueue() done_queue = ClosableQueue() threads = [ StoppableWorker(download, download_queue, resize_queue), StoppableWorker(resize, resize_queue, upload_queue), StoppableWorker(upload, upload_queue, done_queue), ] for thread in threads: thread.start() for _ in range(1000): download_queue.put(object())
download_queue.close()
download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print '%s items finished' % done_queue.qsize()
Copy 要点:
管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条python线程来同事执行这些任务;
构建并发式的管线时,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程、以及如何防止内存膨胀等;
Queue类所提供的机制可以彻底解决上述问题,它具备阻塞式的队列操作、能够指定缓冲区尺寸,而且还支持join方法,这使得开发者可以构建出健壮的管线;
协程 建议6.3 建议使用协程来处理并发场景 Python程序员可以使用线程来运行多个函数,使这些函数看上去好像是在统一时间得到执行,然而,线程有其显著的缺点:
1.多线程的运行协调起来比单线程过程式困难,需要依赖Lock来保证自己的多线程逻辑正确。 2.每个在执行的线程,大约需要8MB内存,在线程处理逻辑较少而数量较多的工程模型中开销较大。 3.线程启动、切换线程上下文的开销较大。
Python的协程(coroutine)可以避免上述问题,它使得Python程序看上去好像是在同时运行多个函数,运行逻辑的调度由程序员自己决定。协程的实现方式,实际上是对生成器的一种扩展。启动生成器协程所需要的开销,与调用函数相仿。处于活跃状态的协程,在其耗尽之前,只会占用到不到1KB的内存。
示例代码: 下面这段代码展示了Python利用yield表达式、生成器来实现的原生协程写法。 grep函数功能为筛选包含关键字的输入。 在定义生成器之后,需要使用next()启动生成器,此时生成器函数,会运行到yield表达式处等待输入。在这之后我们通过生成器的send方法向生成器传递输入,生成器就可以接着yield表达式处向下处理,处理完成后依靠while语句再次等待在yield表达式处。
def grep(pattern): # 生成器函数 ... print("Searching for", pattern) ... while True: ... line = (yield) # yield表达式,生成器函数接收send的输入 ... if pattern in line: ... print(line) ... generator = grep("Python") # 定义生成器 next(generator) # 使用next启动生成器之后,会运行到yield表达式 Searching for Python generator.send("I love Python.") # 给生成器函数发送数据,继续yield处的运行 I love Python. generator.send("I love C++.") generator.send("I love Java.") generator.send("I love Python too.") I love Python too. Copy 如果在以上代码的基础上,连续推进多个独立的生成器,即可模拟出Python线程的并发行为,令程序看上去好像是在同时运行多个函数,同时其消耗相比多线程程序开销要小。
并行 建议6.4 建议使用concurrent.futures实现并行计算 Python程序可以将独立的计算任务分配到多个CPU核上运行,提升并行计算的能力。Python的GIL使得无法使用线程实现真正的并行计算。Python程序的真正并行计算建议采用子进程的方式来实现,具体实现如下: 1.对于并行计算的任务与主进程之间传递的数据比较少,且任务之间不需要共享状态和变量时,采用concurrent.furures的ProcessPoolExecutor类的简单实用方式即可实现并行计算; 2.对于并行计算的场景不满足1)的状态时,可以采用multiprocessing模块提供的共享内存、进程锁、队列、代理等高级功能实现并行计算。因为使用复杂,所以如果不是特性场景,不建议使用这种方式;
使用concurrent.furures的ProcessPoolExecutor类实现并行计算的示例代码如下:
def calc_process(): start = time.time() pool = ProcessPoolExecutor(max_workers=4) results = list(pool.map(gcd, numbers)) end = time.time() print('process calc, Took %.3f seconds' % (end - start)) print(results) Copy 7. 性能 建议7.1 在list成员个数可以预知的情况下,创建list时需预留空间正好容纳所有成员的空间 说明:与Java、C++等语言的list一样,Python语言的list在append()成员时,如果没有多余的空间容纳新的成员,就会分配一块更大的内存,并将原来内存里的成员拷贝到新的内存上,并将最新append()的成员也拷贝到此新内存空间中,然后释放老的内存空间。如果append()调用次数很大,则如上过程会频繁发生,因而会造成灾难性性能下降,而不仅仅是一点下降。
错误示例:
members = [] for i in range(1, 1000000): members.append(i) len(members) Copy 正确示例:
members = [None] * 1000000 for i in range(1, 1000000): members[i] = i len(members) Copy 建议7.2 在成员个数及内容皆不变的场景下尽量使用tuple替代list 说明:list是动态array,而tuple是静态array(其成员个数以及内容皆不可变)。因此,list需要更多的内存来跟踪其成员的状态。
此外,对于成员个数小于等于20的tuple,Python会对其进行缓存,即当此tuple不再使用时,Python并不会立即将其占用的内存返还给操作系统,而是保留以备后用。
错误示例:
myenum = [1, 2, 3, 4, 5] Copy 正确示例:
myenum = (1, 2, 3, 4, 5) # 如果恰好被缓存过,则初始化速度会为错误示例中的5倍以上。 Copy 建议7.3 对于频繁使用的外界对象,尽量使用局部变量来引用之 说明:在Python中对一个函数、变量、模块的调用,是以一种字典树的方式来查找的。Python首先会查找locals()数组,这里保存着所有的局部变量;如果找不到,则会继续查找globals()数组;如果在这里也找不到,则会到buildtin(其实是一个模块)中的locals()数组中查找,或者到其它import进来的模块/类中查找。
错误示例:
import math def afunc(): for x in xrange(100000): return math.tan(x) Copy 在这个例子中,Python会先到globals()中的名值对字典中,找到math模块;然后在math模块的locals()的字典中查找tan()函数;然后在当前函数的locals()中查找x。这里存在着3次查找。
当调用次数大时,每次调用多出来的1、2次查找,就会被放大。
错误示例:
from math import tan def afunc(): for x in xrange(100000): return tan(x) Copy 在这个例子中,Python会先到globals()的字典中查找tan()函数(其已经被from math import tan语句加载到了globals()中);然后在当前函数的locals()中查找x。这里存在着2次查找,比前一个例子少了一次查找,但是还不是最优解。
正确示例:
import math def afunc(tan=math.tan): for x in xrange(100000): return tan(x) Copy 在这个例子中,在函数定义时,有且只有一次查找math模块、然后查找tan函数的操作;之后在循环中对tan()函数的调用,都是在afunc()函数的locals()中进行查找,而对函数的locals()中的查找,Python是有特殊优化措施的,速度是非常快的;当然,还包括对本地变量x的查找(也是在当前函数的locals()中查找)。
建议7.4 Python2.x中使用xrange代替range 说明:在Python中,for x in range(1, 10000),等价于Java/C/C++中的for (int i = 0; i < 10000; i++),非常常用。但是,range其实等价于如下定义:
def range(begin, end, step=1): indices = [] while begin < end: indices.append(begin) start += step Copy 在此过程中,indices数组可能会有内存效率问题,使得range构造枚举值的代价高昂。 xrange等价于如下定义:
def range(begin, end, step=1): while begin < end: yield begin start += step Copy 在这个过程中,并没有一个大数组的生成。每次for循环都只会向xrange要一个数据,xrange拥有且也只有一个当前数据给for循环。因此,空间占用非常小,并且还不存扩容时在分配新内存、拷贝老内存内容、释放老内存的操作步骤。
错误示例:
for x in range(1, 1000000): print x Copy 正确示例:
for x in xrange(1, 1000000): print x Copy 建议7.5 尽量使用generator comprehension代替list comprehension 说明:list comprehension可以用来代替lambda表达式的map、reduce语法,从已有的list中,生成新的数据。而generator comprehension无需定义一个包含yield语句的函数,就可以生成一个generator。 二者一个生成list,另外一个生成generator,在内存的占用上,相差悬殊;在生成速度上,相差无几。
错误示例:
even_cnt = len([x for x in range(10) if x % 2 == 0]) Copy 正确示例:
even_cnt = sum(1 for x in range(10) if x % 2 == 0) Copy 建议7.6 使用format方法、"%"操作符和join方法代替"+"和"+="操作符来完成字符串格式化 说明:即使参数都是字符串,也可以使用format方法或%运算符来格式化字符串。一般性能要求的场景可以使用+或+=运算符,但需要避免使用+和+=运算符在循环中累积字符串。由于字符串是不可变的,因此会产生不必要的临时对象并导致二次而非线性运行时间。
推荐做法:
x = '%s, %s!' % (imperative, expletive) x = '{}, {}!'.format(imperative, expletive) x = 'name: %s; score: %d' % (name, n) x = 'name: {}; score: {}'.format(name, n) items = ['
'] for last_name, first_name in employee_list: items.append('' % (last_name, first_name)) items.append('| %s, %s |
x = imperative + ', ' + expletive + '!' x = 'name: ' + name + '; score: ' + str(n) employee_table = '
' for last_name, first_name in employee_list: employee_table += '' % (last_name, first_name) employee_table += '| %s, %s |
错误示例:
import sys
def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2)
def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) # python2 能够成功执行,但是python3 会报错
bad() Copy 在Python 2运行结果:
python foo.py 2 value error 2 Copy 但是在Python 3里:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment Copy 正确示例:
import sys
def bar(i): if i == 1:
KeyError(1) if i == 2: raise ValueError(2)
def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception)
good() Copy 在Python 3中运行结果:
python3 foo.py 2 value error 2 Copy 9. 编程实践 规则9.1 函数参数中的可变参数不要使用默认值,在定义时使用None 说明:参数的默认值会在方法定义被执行时就已经设定了,这就意味着默认值只会被设定一次,当函数定义后,每次被调用时都会有"预计算"的过程。当参数的默认值是一个可变的对象时,就显得尤为重要,例如参数值是一个list或dict,如果方法体修改这个值(例如往list里追加数据),那么这个修改就会影响到下一次调用这个方法,这显然不是一种好的方式。应对种情况的方式是将参数的默认值设定为None。
错误示例:
def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar Copy 在上面这段代码里,一旦重复调用foo()函数(没有指定一个bar参数),那么将一直返回'bar'。因为没有指定参数,那么foo()每次被调用的时候,都会赋予[]。下面来看看,这样做的结果:
foo() ["baz"] foo() ["baz", "baz"] foo() ["baz", "baz", "baz"] Copy 正确示例:None是不错的选择
def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... foo() ["baz"] foo() ["baz"] foo() ["baz"] Copy 规则9.2 对子类继承的变量要做显式定义和赋初值 说明:在Python中,类变量都是作为字典进行内部处理的,并且遵循方法解析顺序(MRO)。子类没有定义的属性会引用基类的属性值,如果基类的属性值发生变化,对应的子类引用的基类的属性的值也相应发生了变化。
错误示例:
class A(object): x = 1
class B(A): pass
class C(A): pass
B.x = 2 print A.x, B.x, C.x 1 2 1
A.x = 3 print A.x, B.x, C.x 3 2 3 Copy 这里虽然没有给C.x赋值,但是由于基类的值A.x发生改变,在获取C.x的值得时候发现它引用的数据发生了变化。在上面这段代码中,因为属性x没有在类C中发现,它会查找它的基类(在上面例子中只有A,尽管Python支持多继承)。换句话说,就是C自己没有x属性,因此,引用C.x其实就是引用A.x。
正确示例:如果希望类C中的x不引用自A类,可以在C类中重新定义属性X,这样C类的就不会引用A类的属性x了,值的变化就不会相互影响。
class B(A): x = 2 class C(A): x = -1
print A.x, B.x, C.x 1 2 -1
A.x = 3 print A.x, B.x, C.x 3 2 -1 Copy 规则9.3 严禁使用注释行等形式仅使功能失效 说明:python的注释包含:单行注释、多行注释、代码间注释、doc string等。除了doc string是使用""""""括起来的多行注释,常用来描述类或者函数的用法、功能、参数、返回等信息外,其余形式注释都是使用#符号开头用来注释掉#后面的内容。基于python语言运行时编译的特殊性,如果在提供代码的时候提供的是py文件,即便是某些函数和方法在代码中进行了注释,别有用心的人依然可以通过修改注释来使某些功能启用;尤其是某些接口函数,如果不在代码中进行彻底删除,很可能在不知情的情况下就被启用了某些本应被屏蔽的功能。因此根据红线要求,在python中不使用的功能、模块、函数、变量等一定要在代码中彻底删除,不给安全留下隐患。即便是不提供源码py文件,提供编译过的pyc、pyo文件,别有用心的人可以通过反编译来获取源代码,可能会造成不可预测的结果。
错误示例:在main.py中有两个接口被注释掉了,但是没有被删除。
if name == "main": if sys.argv[1].startswith('--'): option = sys.argv[1][2:] if option == "load": #安装应用 LoadCmd(option, sys.argv[2:3][0]) elif option == 'unload': #卸载应用 UnloadCmd(sys.argv[2:3][0]) elif option == 'unloadproc': #卸载流程 UnloadProcessCmd(sys.argv[2:3][0])
elif option == 'active':
ActiveCmd(sys.argv[2:3][0])
elif option == 'inactive':
InActiveCmd(sys.argv[2:3][0])
else:
Loginfo("Command %s is unknown"%(sys.argv[1]))
Copy 在上例中很容易让其他人看到我们程序中的两个屏蔽的接口,容易造成不安全的因素,注释的代码应该删除。
if name == "main": if sys.argv[1].startswith('--'): option = sys.argv[1][2:] if option == "load": #安装应用 LoadCmd(option, sys.argv[2:3][0]) elif option == 'unload': #卸载应用 UnloadCmd(sys.argv[2:3][0]) elif option == 'unloadproc': #卸载流程 UnloadProcessCmd(sys.argv[2:3][0]) else: Loginfo("Command %s is unknown"%(sys.argv[1])) Copy 建议9.4 慎用copy和 deepcopy 说明:在python中,对象赋值实际上是对象的引用。当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用。如果需要拷贝对象,需要使用标准库中的copy模块。copy模块提供copy和deepcopy两个方法:
copy浅拷贝:拷贝一个对象,但是对象的属性还是引用原来的。对于可变类型,比如列表和字典,只是复制其引用。基于引用所作的改变会影响到被引用对象。 deepcopy深拷贝:创建一个新的容器对象,包含原有对象元素(引用)全新拷贝的引用。外围和内部元素都拷贝对象本身,而不是引用。 Notes:对于数字,字符串和其他原子类型对象等,没有被拷贝的说法。如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。使用copy和deepcopy时,需要了解其使用场景,避免错误使用。
示例:
import copy a = [1, 2, ['x', 'y']] b = a c = copy.copy(a) d = copy.deepcopy(a) a.append(3) a[2].append('z') a.append(['x', 'y']) print a [1, 2, ['x', 'y', 'z'], 3, ['x', 'y']] print b [1, 2, ['x', 'y', 'z'], 3, ['x', 'y']] print c [1, 2, ['x', 'y', 'z']] print d [1, 2, ['x', 'y']] Copy 规则9.5 使用os.path库中的方法代替字符串拼接来完成文件系统路径的操作 说明:os.path库实现了一系列文件系统路径操作方法,这些方法相比单纯的路径字符串拼接来说更为安全,而且为用户屏蔽了不同操作系统之间的差异。
错误示例:如下路径字符串的拼接在Linux操作系统无法使用
path = os.getcwd() + '\test.txt' Copy 正确示例:
path = os.path.join(os.getcwd(), 'test.txt') Copy 建议9.6 使用subprocess模块代替os.system模块来执行shell命令 说明:subprocess模块可以生成新进程,连接到它们的input/output/error管道,并获取它们的返回代码。该模块旨在替换os.system等旧模块,相比os.system模块来说更为灵活。
推荐做法:
subprocess.run(["ls", "-l"]) # doesn't capture output CompletedProcess(args=['ls', '-l'], returncode=0)
subprocess.run("exit 1", shell=True, check=True) Traceback (most recent call last): ... subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
subprocess.run(["ls", "-l", "/dev/null"], capture_output=True) CompletedProcess(args=['ls', '-l', '/dev/null'], returncode=0, stdout=b'crw-rw-rw- 1 root root 1, 3 Jan 23 16:23 /dev/null\n', stderr=b'') Copy 建议9.7 建议使用with语句操作文件 说明:Python 对一些内建对象进行改进,加入了对上下文管理器的支持,可以用于 with 语句中。使用 with 语句可以自动关闭文件,减少文件读取操作错误的可能性,在代码量和健壮性上更优。注意 with 语句要求其操作的类型实现"enter()"和"exit()"方法,需确认实现后再使用。
推荐做法:
with open(r'somefileName') as somefile: for line in somefile: print line # ...more code Copy 此使用with语句的代码等同于以下使用try...finally...结构的代码。
somefile = open(r'somefileName') try: for line in somefile: print line # ...more code finally: somefile.close() Copy 在代码量和健壮性上with结构都要优于后者。