这个周末断断续续的阅读完了《Effective Python之编写高质量Python代码的59个有效方法》,感觉还不错,具有很大的指导价值。 下面将以最简单的方式记录这59条建议,并在大部分建议后面加上了说明和示例,文章篇幅大,请您提前备好瓜子和啤酒!
第一条:确认自己使用的Python版本
(1)有两个版本的python处于活跃状态,python2和python3
(2)有很多流行的Python运行时环境,CPython、Jython、IronPython以及PyPy等
(3)在开发项目时,应该优先考虑Python3
第二条:遵循PEP风格指南
PEP8是针对Python代码格式而编订的风格指南,参考: http://www.python.org/dev/peps/pep-0008
(1)当编写Python代码时,总是应该遵循PEP8风格指南
(2)当广大Python开发者采用同一套代码风格,可以使项目更利于多人协作
(3)采用一致的风格来编写代码,可以令后续的修改工作变得更为容易
第三条:了解bytes、str、与unicode的区别
(1)python2提供str个unicode,python3中修改为bytes和str,bytes为原始的8位值,str包含unicode字符,在进行编码转换时使用decode和encode方法
(2)从文件中读取二进制数据,或向其中写入二进制数据时,总应该以‘rb’或‘wb’等二进制模式来开启文件
第四条:用辅助函数来取代复杂的表达式
(1)开发者很容易过度运用Python的语法特性,从而写出那种特别复杂并且难以理解的单行表达式
(2)请把复杂的表达式移入辅助函数中,如果要反复使用相同的逻辑,那更应该这么做
第五条:了解切割序列的方法
(1)不要写多余的代码:当start索引为0,或end索引为序列长度时,应将其省略a[:]
(2)切片操作不会计较start与end索引是否越界,者使得我们很容易就能从序列的前端或后端开始,对其进行范围固定的切片操作,a[:20]或a[-20:]
(3)对list赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换
第六条:在单词切片操作内,不要同时指导start、end和step
(1)这条的目的主要是怕代码难以阅读,作者建议将其拆解为两条赋值语句,一条做范围切割,另一条做步进切割
(2)注意:使用[::-1]时会出现不符合预期的错误,看下面的例子
msg = '谢谢' print('msg:',msg) x = msg.encode('utf-8') y = x.decode('utf-8') print('y:',y) z=x[::-1].decode('utf-8') print('z:', z)
输出:
第七条:用列表推导式来取代map和filter
(1)列表推导要比内置的map和filter函数清晰,因为它无需额外编写lambda表达式
(2)字典与集合也支持推导表达式
第八条:不要使用含有两个以上表达式的列表推导式
第九条:用生成器表达式来改写数据量较大的列表推导式
(1)列表推导式的缺点
在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表,当输入的数据比较少时,不会出现问题,但如果输入数据非常多,那么可能会消耗大量内存,并导致程序崩溃,面对这种情况,python提供了生成器表达式,它是列表推导和生成器的一种泛化,生成器表达式在运行的时候,并不会把整个输出序列呈现出来,而是会估值为迭代器。
把实现列表推导式所用的那种写法放在一对园括号中,就构成了生成器表达式
numbers = [1,2,3,4,5,6,7,8] li = (i for i in numbers) print(li)
(2)串在一起的生成器表达式执行速度很快
第十条:尽量用enumerate取代range
(1)尽量使用enumerate来改写那种将range与下表访问结合的序列遍历代码
(2)可以给enumerate提供第二个参数,以指定开始计数器时所用的值,默认为0
color = ['red','black','write','green'] #range方法 for i in range(len(color)): print(i,color[i]) #enumrate方法 for i,value in enumerate(color): print(i,value)
第11条:用zip函数同时遍历两个迭代器
(1)内置的zip函数可以平行地遍历多个迭代器
(2)Python3中的zip相当于生成器,会在遍历过程中逐次产生元组,而python2中的zip则是直接把这些元组完全生成好,并一次性地返回整份列表、
(3)如果提供的迭代器长度不等,那么zip就会自动提前终止
attr = ['name','age','sex'] values = ['zhangsan',18,'man'] people = zip(attr,values) for p in people: print(p)
第12条:不要在for和while循环后面写else块
(1)python提供了一种很多编程语言都不支持的功能,那就是在循环内部的语句块后面直接编写else块
for i in range(3): print('loop %d' %(i)) else: print('else block!')
上面的写法很容易让人产生误解:如果循环没有正常执行完,那就执行else,实际上刚好相反
(2)不要再循环后面使用else,因为这种写法既不直观,又容易让人误解
第13条:合理利用try/except/else/finally结构中的每个代码块
try: #执行代码 except: #出现异常 else: #可以缩减try中代码,再没有发生异常时执行 finally: #处理释放操作
第14条:尽量用异常来表示特殊情况,而不要返回None
(1)用None这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为None和0及空字符串之类的值,在表达式里都会贝评估为False
(2)函数在遇到特殊情况时应该抛出异常,而不是返回None,调用者看到该函数的文档中所描述的异常之后,应该会编写相应的代码来处理它们
第15条:了解如何在闭包里使用外围作用域中的变量
(1)理解什么是闭包
闭包是一种定义在某个作用域中的函数,这种函数引用了那个作用域中的变量
(2)表达式在引用变量时,python解释器遍历各作用域的顺序:
a. 当前函数的作用域
b. 任何外围作用域(例如:包含当前函数的其他函数)
c. 包含当前代码的那个模块的作用域(也叫全局作用域)
d. 内置作用域(也即是包含len及str等函数的那个作用域)
e. 如果上卖弄这些地方都没有定义过名称相符的变量,那么就抛出NameError异常
(3)赋值操作时,python解释器规则
给变量赋值时,如果当前作用域内已经定义了这个变量,那么该变量就会具备新值,若当前作用域内没有这个变量,python则会把这次赋值视为对该变量的定义
(4)nonlocal
nonlocal的意思:给相关变量赋值的时候,应该在上层作用域中查找该变量,nomlocal的唯一限制在于,它不能延申到模块级别,这是为了防止它污染全局作用域
(5)global
global用来表示对该变量的赋值操作,将会直接修改模块作用域的那个变量
第16条:考虑用生成器来改写直接返回列表的函数
参考第九条
第17条:在参数上面迭代时,要多加小心
(1)函数在输入的参数上面多次迭代时要当心,如果参数是迭代对象,那么可能会导致奇怪的行为并错失某些值
看下面两个例子:
例1:
def normalize(numbers): total = sum(numbers) print('total:',total) print('numbers:',numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result
numbers = [15,35,80] print(normalize(numbers))
输出:
例2:将numbers换成生成器
def fun(): li = [15,35,80] for i in li: yield i
print(normalize(fun()))
输出:
原因:迭代器只产生一轮结果,在抛出过StopIteration异常的迭代器或生成器上面继续迭代第二轮,是不会有结果的。
(2)python的迭代器协议,描述了容器和迭代器应该如何于iter和next内置函数、for循环及相关表达式互相配合
(3) 想判断某个值是迭代器还是容器 ,可以拿该值为参数,两次调用iter函数,若结果相同,则是迭代器,调用内置的next函数,即可令该迭代器前进一步
if iter(numbers) is iter(numbers): raise TypeError('Must supply a container')
第18条:用数量可变的位置参数减少视觉杂讯
(1)在def语句中使用*args,即可令函数接收数量可变的位置参数
(2)调用函数时,可以采用*操作符,把序列中的元素当成位置参数,传给该函数
(3)对生成器使用操作符,可能导致程序耗尽内存并崩溃,所以只有当我们能够确定输入的参数个数比较少时,才应该令函数接受arg式的变长参数
(4) 在已经接收*args参数的函数上面继续添加位置参数 ,可能会产生难以排查的错误
第19条:用关键字参数来表达可选的行为
(1)函数参数可以按位置或关键字来指定
(2)只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键字参数则能够阐明每个参数的意图
(3)该函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数调用代码保持兼容
(4) 可选的关键字参数 总是应该以关键字形式来指定,而不应该以位置参数来指定
第20条:用None和文档字符串来描述具有动态默认值的参数
import datetime import time def log(msg,when=datetime.datetime.now()): print('%s:%s' %(when,msg))
log('hi,first') time.sleep(1) log('hi,second')
输出:
两次显示的时间一样,这是因为datetime.now()只执行了一次,也就是它只在函数定义的时候执行了一次,参数的默认值,会在每个模块加载进来的时候求出,而很多模块都在程序启动时加载。我们可以将上面的函数改成:
import datetime import time def log(msg,when=None): """ arg when:datetime of when the message occurred """
if when is None: when=datetime.datetime.now() print('%s:%s' %(when,msg))
log('hi,first') time.sleep(1) log('hi,second')
输出:
(1)参数的默认值,只会在程序加载模块并读到本函数定义时评估一次,对于{}或[]等动态的值,这可能导致奇怪的行为
(2)对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为None,并在函数的文档字符串里面描述该默认值所对应的实际行为
第21条:用只能以关键字形式指定的参数来确保代码明确
(1)关键字参数能够使函数调用的意图更加明确
(2)对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接收多个Boolean标志的函数更应该这样做
第22条:尽量用辅助类来维护程序的状态,而不要用字典或元组
作者的意思是:如果我们使用字典或元组保存程序的某部分信息,但随着需求的不断变化,需要逐渐的修改之前定义好的字典或元组结构,会出现多次的嵌套,过分膨胀会导致代码出现问题,而且难以理解。遇到这样的情况,我们可以把嵌套结构重构为类。
(1)不要使用包含其他字典的字典,也不要使用过长的元组
(2)如果容器中包含简单而又不可变的数据,那么可以先使用namedtupe来表述,待稍后有需要时,再修改为完整的类
注意:namedtuple类无法指定各参数的默认值,对于可选属性比较多的数据来说,namedtuple用起来不方便
(3)保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆分为多个辅组类
第23条:简单的接口应该接收函数,而不是类的实例
(1)对于连接各种python组件的简单接口来说,通常应该给其直接传入函数,而不是先定义某个类,然后再传入该类的实例
(2)Python种的函数和方法可以像类那么引用,因此,它们与其他类型的对象一样,也能够放在表达式里面
(3)通过名为call的特殊方法,可以使类的实例能够像普通的Python函数那样得到调用
第24条:以@classmethod形式的多态去通用的构建对象
在python种,不仅对象支持多态,类也支持多态
(1)在Python程序种,每个类只能有一个构造器,也就是init方法
(2)通过@classmethod机制,可以用一种与构造器相仿的方式来构造类的对象
(3)通过类方法机制,我们能够以更加通用的方式来构建并拼接具体的子类
下面以实现一套MapReduce流程计算文件行数为例来说明:
(1)思路
(2)上代码
import threading import os class InputData: def read(self): raise NotImplementedError class PathInputData(InputData): def init(self,path): super().init() self.path = path
def read(self): return open(self.path).read()
class worker: def init(self,input_data): self.input_data = input_data self.result = None
def map(self): raise NotImplementedError
def reduce(self): raise NotImplementedError
class LineCountWorker(worker): def map(self): data = self.input_data.read() self.result = data.count('n')
def reduce(self,other): self.result += other.result
def generate_inputs(data_dir): for name in os.listdir(data_dir): yield PathInputData(os.path.join(data_dir,name))
def create_workers(input_list): workers = [] for input_data in input_list: workers.append(LineCountWorker(input_data)) return workers
def execute(workers): threads = [threading.Thread(target=w.map) for w in workers] for thread in threads: thread.start() for thread in threads: thread.join()
first,rest = workers[0],workers[1:] for worker in rest: first.reduce(worker) return first.result
def mapreduce(data_dir): inputs = generate_inputs(data_dir) workers = create_workers(inputs) return execute(workers)
if name == "main": print(mapreduce('D:mapreduce_test')) MapReduce
上面的代码在拼接各种组件时显得非常费力,下面重新使用@classmethod来改进下
import threading import os class InputData: def read(self): raise NotImplementedError
@classmethod def generate_inputs(cls,data_dir): raise NotImplementedError class PathInputData(InputData): def init(self,path): super().init() self.path = path
def read(self): return open(self.path).read()
@classmethod def generate_inputs(cls,data_dir): for name in os.listdir(data_dir): yield cls(os.path.join(data_dir,name))
class worker: def init(self,input_data): self.input_data = input_data self.result = None
def map(self): raise NotImplementedError
def reduce(self): raise NotImplementedError
@classmethod def create_workers(cls,input_list): workers = [] for input_data in input_list: workers.append(cls(input_data)) return workers
class LineCountWorker(worker): def map(self): data = self.input_data.read() self.result = data.count('n')
def reduce(self,other): self.result += other.result
def execute(workers): threads = [threading.Thread(target=w.map) for w in workers] for thread in threads: thread.start() for thread in threads: thread.join()
first,rest = workers[0],workers[1:] for worker in rest: first.reduce(worker) return first.result
def mapreduce(data_dir): inputs = PathInputData.generate_inputs(data_dir) workers = LineCountWorker.create_workers(inputs) return execute(workers)
if name == "main": print(mapreduce('D:mapreduce_test'))
修改后的MapReduce
通过类方法实现多态机制,我们可以用更加通用的方式来构建并拼接具体的类
第25条:用super初始化父类
如果从python2开始详细的介绍super使用方法需要很大的篇幅,这里只介绍python3中的使用方法和MRO
(1)MRO即为方法解析顺序,以标准的流程来安排超类之间的初始化顺序,深度优先,从左至右,它也保证钻石顶部那个公共基类的init方法只会运行一次
(2)python3中super的使用方法
python3提供了一种不带参数的super调用方法,该方式的效果与用class和self来调用super相同
class A(Base): def init(self,value): super(class,self).init(value)
class A(Base): def init(self,value): super().init(value)**
推荐使用上面两种方法,python3可以在方法中通过class变量精确的引用当前类,而Python2中则没有定义class方法
(3)总是应该使用内置的super函数来初始化父类
第26条:只在使用Mix-in组件制作工具类时进行多重继承
python是面向对象的编程语言,它提供了一些内置的编程机制,使得开发者可以适当地实现多重继承,但是,我们应该尽量避免多重继承,若一定要使用,那就考虑编写mix-in类,mix-in是一种小型的类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的 实例属性,此外,它也不要求使用者调用自己的init函数
(1)能用mix-in组件实现的效果,就不要使用多重继承来做
(2)将各功能实现为可插拔的mix-in组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所具备的行为
(3)把简单的行为封装到mix-in组件里,然后就可以用多个mix-in组合出复杂的行为了
第27条:多用public属性,少用private属性
python没有从语法上严格保证private字段的私密性,用简单的话来说,我们都是成年人。
个人习惯:_XXX 单下划代表protected;XXX 双下划线开始的且不以_结尾表示private;XXX__系统定义的属性和方法
class People: __name="zhanglin"
def init(self): self.__age = 16
print(People.dict) p = People() print(p.dict)
会发现name和age属性名都发生了变化,都变成了(_类名+属性名), 只有在__XXX这种命名方式下才会发生变化,所以以这种方式作为伪私有说明
(1)python编译器无法严格保证private字段的私密性
(2)不要盲目地将属性设为private,而是应该从一开始就做好规划,并允许子类更多地访问超类内部的api
(3)应该更多的使用protected属性,并在文档中把这些字段的合理用法告诉子类的开发者,而不是试图用private属性来限制子类访问这些字段
(4)只有当子类不受自己控制时,才可以考虑用private属性来避免名称冲突
第28条:继承collections.abc以实现自定义的容器类型
collections.abc模块定义了一系列抽象基类,它们提供了每一种容器类型所应具备的常用方法,大家可以自己参考源码
all = ["Awaitable", "Coroutine", "AsyncIterable", "AsyncIterator", "AsyncGenerator", "Hashable", "Iterable", "Iterator", "Generator", "Reversible", "Sized", "Container", "Callable", "Collection", "Set", "MutableSet", "Mapping", "MutableMapping", "MappingView", "KeysView", "ItemsView", "ValuesView", "Sequence", "MutableSequence", "ByteString", ]**
(1)如果定制的子类比较简单,那就可以直接从Python的容器类型(如list、dict)中继承
(2)想正确实现自定义的容器类型,可能需要编写大量的特殊方法
(3)编写自制的容器类型时,可以从collections.abc模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为