《Effective Python 编写高质量Python代码的59个有效方法》这本书是python进阶阶段可以阅读的书,不适合初学者,及时是进阶阶段,也建议放到中期来读,即不适合作为python进阶的第一本书,之前介绍的《流畅的python》比较适合进阶第一书阅读。
另外,这本书没有多少原理性说明,更多的是技巧或者编程习惯方面的建议。不要期望多高。
第1条:确认自己所用的python版本
import sys
print(sys.version_info)
print(sys.version)
python2到python3的自动化迁移工具有:2to3, six等
第2条:遵循PEP8风格指南
PEP 8应该读一遍,并遵守:https://www.python.org/dev/peps/pep-0008/
空白:
每行字符数不应该超过79个
函数与类之间的间隔应该有2个空行隔开
在同一个类中的方法之间应该有1个空行隔开
变量赋值的=号左右需要空一格
[]取值里面不要有空格
命名:
函数、变量、属性应该是小写字母,单词之间有下划线
受保护的实例属性应该以单_开头
私有属性以两个__开头
类与异常,应该以每个单词大写,且没有_组成
模块级别的常量,应该以全大写字母且有_相连
类方法的第一个参数应该是cls,实例方法第一个参数是self
表达式和语句:
采用内联形式的否定词,而不要放到整个表达式前面,如if a is not b,不是if not a is b
不用用长度检测,如if len(somelist) == 0来判断[]或’'为空,应直接采用if not somelist。空值会自动认为是False
不要单行的if/for/while/except语句
引入魔抗应该使用绝对名称,from package import module
import应该分为3部分:标准库、第三方库、自己的模块,各import可以按字母顺序排序
第3条:了解bytes、str、unicode区别
python的str就是unicode,简称码位,程序体中应该以unicode为主,在入口和出口处做编解码处理
python3打开文件默认是utf-8编码格式
第4条:用辅助函数来取代复杂的表达式
当一个表达式很长很复杂时,把它拆成函数实现
第5条:了解切割序列的方法
[]中的开头和结尾如非必要空着,[]内最好只有一个元素
第6条:在单次切片操作内,不要同时指定start、end和stride
str[::-1]可以获取字符串的倒序
第7条:用列表推导来取代map和filter
第8条:不要使用含有两个以上的表达式的列表推导
第9条:用生成器表达式来改写数据量较大的列表推导
第10条:尽量用enumarate取代range
第11条:用zip函数同时遍历2个迭代器
第12条:不要在for和while循环后面写else块
else其实是没有错误时执行
第13条:合理利用try/except/else/finally结构中的每一个代码块
第14条:尽量用异常来表示特殊情况,而不要返回None
避免返回None的2种办法:
1、把返回值拆成2个组成的元祖,第一个表示操作是否成功True或False,第二个表示真实结果
2、抛出异常给上一级,raise ValueError(“Invalid inputs”) from e
第15条:了解如何在闭包里使用外围作用域种的变量
nonlocal只能作用在当前和上一层的函数体中
第16条:考虑用生成器来改写直接返回列表的函数
将迭代器用list()可以返回所有值
第17条:在参数上面迭代时,要多加小心
在已经用完的迭代上面继续使用不会报错
第18条:用数量可变的位置参数减少视觉杂讯
第19条:用关键字参数来表达可选的行为
位置参数必须出现在关键字参数前面
优点:
提高可读性
提供默认值
提高可扩展性
第20条:用None和文档字符串来描述具有动态默认值的参数
参数的默认值只会在模块加载时被执行一次,后面不会再执行
参数的默认值如果用可修改值,会引入所有引用该函数的地方都使用同一个值,比如[]{}
第21条:用只能以关键字形式指定的参数来确保代码清晰
python3中定义一种只能以关键字形式来指定的参数,从而确保调用该函数的代码读起来会比较明确。
这些参数必须以关键字的形式提供,而不能按位置提供。
在参数列表中用*完成位置参数和关键字参数的分隔
第22条:尽量用辅助类来维护程序的状态,而不要用字典和元祖
用来保存程序状态的数据结构一旦变得过于复杂,就应该将其拆解为类,以便提供更为明确的接口,并更好的封装数据。
这样做也能在接口和具体实现之间创建抽象层。
namedtuple的局限:无法指定默认值。能用类尽量用类。
不要使用包含其他字典的字典,也不要使用过长的元组
如果容器中包含简单而又不可变的数据,那么可用先用namedtuple来表示,以后再改为类。
保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类
第23条:简单的接口应该接受函数,而不是类的实例
通过__call__特殊方法,可以使类的实例能够被调用
如果要用函数来保存状态,那就应该定义新的类,病实现__call__方法
第24条:以@classmethod形式的多态去通用的构建对象
python只允许__init__的构造方法,但通过@classmethod可以完成多种构造方法
第25条:用super初始化父类
类的继承顺序,深度优先,从左到右
super().init()
第26条:只在使用Mix-in组件制作工具类时进行多重继承
Mix-in是一种小型的类,只定义其他类需要的一套方法,而不定义自己的实例属性。
也不要求调用自己的构造器
Mix-in最大优势是使用者可以随时安插这些通用的功能,并在必要时覆盖它们
第27条:多用public,少用private属性
子类无法访问父类的private字段
第28条:继承collections.abc以实现自定义的容器类型、
from collections.abc import Sequence
第29条:用纯属性取代get和set方法
@property和 @setter方法实现get和set方法
使用@func.setter后,即使是__init__中的赋值操作也会运行setter装饰方法、
@property最大缺点是和属性相关的方法,只能在子类里面共享,其他类无法复用这些代码
@propery的执行要快速返回,时间长的操作应该放到普通函数中
第30条:考虑用@proerty来替代属性重构
第31条:用描述符来改写复用的@proerty方法
描述符能够把同一套逻辑运用在类中的不同属性中,从这个角度看,描述符比mix-in好一些
WeakKeyDictionnary弱引用字典特性,key或value不存在时,可以自动回收,不浪费内存
第32条:用__getattr__,getattribute,__setattr__实现按需生成的属性、
第33条:用元类来验证子类
定义元类的时候要从type中继承,而从对于使用该元类的其他类来说,python默认会把这些类的class语句体中所含的相关内容,发送给元类的__new__方法
第34条:用元类来注册子类
第35条:用元类来注解类的属性
第36条:用subprocess模块来管理子进程
并发和并行的本质区别是能不能提速
多个子进程是可以并行处理的
最简单、最好用的子进程管理模块就是subprocess
子进程会独立于父进程运行,这里的父进程指python解释器
chain就是把第一个子进程的输出,与第二个子进程的输入联系起来
第37条:可以用县城来执行阻塞式I/O,但不要用它做平行计算
受GIL保护,同一时刻只有一条线程可以向前执行
python支持线程的原因:
1、多线程使得程序看上去好像能够在同一时间做许多事情
2、处理阻塞式I/O操作
GIL虽然使得python代码无法并行,但它对系统调用却没有任何负面影响。由于python线程在执行系统调用的时候回释放GIL,并一直
等到执行完毕才会重新获取它,所以GIL是不会影响系统调用的。
协程asynio
第38条:在线程中使用lock来防止数据竞争
GIL并不会保护开发者自己编写的代码
线程切换是会破坏正在写数据的动作
冲突原因:
为了保证所有的线程都能够公平的执行,python解释器会给每个线程分配大致相等的处理器时间。
为了达到这样的目的,python系统可能当某个线程正在执行的时候,将其暂停,然后执行另一个线程。
Lock类,相当于互斥锁
lock = Lock()
with lock:
count += 1
第39条:用Queue来协调各线程之间的工作
Queue类使得工作线程无需频繁的查询输入队列的状态,因为它的get()方法会持续阻塞,直到有新的数据加入
queue = Queue()
queue.get()
queue.put(object())
用queue.task_done()来跟踪工作进度
第40条:考虑用协程并发的运行多个函数
线程有3个显著缺点:
1、为了确保数据安全,我们必须使用特殊工具来协调这些线程,多线程难维护
2、线程占用大量内存,每个线程大约占用8MB
3、线程启动时的开销比较大
协程coroutine可以解决线程的这些问题,它使得python看上去像同时运行多个函数。
协程的实现方式,实际上是对生成器的一种扩展
第41条:考虑用concurrent.futures来实现真正的并行计算
ProcessPoolExecutor类利用multiprocessing模块提供的底层机制,做了大量的封装工作,比如:
主进程和子进程之间的通讯
multiprocessing的开销之所以比较大,原因就在于主进程和子进程之间必须进行序列化和反序列化操作,而程序中的大量开销,正是由这些操作所引发的。
对于某些较为孤立的,且数据利用率较高的任务,这套方案非常合适。
如果不满足于multiprocessing的开销,可以用它提供的高级机制:共享内存shared memory, 跨进程锁定cross-process lcok, 队列queue, 代理proxy等。
这些特性用起来比较复杂
第42条:用functools.wraps定义函数的修饰器
装饰器作用:
对于被装饰的函数,装饰器能够在哪个函数执行之前及执行完毕之后,分别运行一些附加代码。这使得开发者可以在装饰器里面访问并修改原函数及返回值,
以实现约束语义、调试程序、注册函数等目标
普通的装饰器函数有下面缺点:
1、用%r打印原函数,会出现装饰器函数
2、对于调试器、对象序列化器等需要使用内省机制那些工具,会被影响。比如help()函数
functools.wraps装饰器会把内部函数相关的重要元数据全部复制到外围函数。
def deco(func):
@functools.wraps
def wrapper(*args, **kwargs):
…
return wrapper
@deco
def fibonacci(n)
第43条:考虑以contextlib和with语句来改写可复用的try/finally代码
contextlib模块提供了名为contextmanager装饰器,一个简单的函数只需要经过contextmanager装饰,即可用在with语句之中。
这个函数之内需要实现yield语句
@contextmanager
def func()
…
yield
…
情景管理器可以通过yield语句向with语句返回一个值,此值会赋值给as关键字指定变量。
另外一种类的实现方式是实现__enter__和__exit__函数
第44条:用copyreg实现可靠的pickle操作
pickle的设计目标是提供一种二进制渠道,使开发者能够在自己所控制的各程序之间传递python对象
彼此信任的程序之间通讯可以使用pickle,但不信任的时候需要使用json。原因如下:
由pickle模块所产生的序列化数据采用的是一种不安全的格式,实际上就是一个程序,描述如何来构建原始的python对象。
这意味着如果pickle数据被恶意修改,在反序列化时候会对程序造成影响
而json采用的是一种安全格式,只是包含简单的描述信息,描述由对象所构成的体系。
举例:
pickle.dump(obj, fp)
pickle.load(pick_obj)
copyreg内置函数是用于配合pickle使用的,
copyreg.pickle(GameState, pickle_game_state),后面对Gamestate对象执行序列化时候,会调用pickle_game_state方法
pickle_game_state方法会返回一个元祖(反序列化函数,对象构造参数)
数据还原有可能遇到属性确实情况,解决方法有:
1、为缺失的属性提供默认值,就是类的构造函数的参数使用默认值,这样在反序列化缺失参数会用默认值替代
2、用版本号来管理类。在序列化时加入一个version字段,反序列化时根据version进行微调。kwargs.pop('version',1)
3、固定的引入路径。普通的pickle函数里面有类名,如果类名变化了之后就无法再反序列化了。
而regcopy注册的序列化函数序列化后的数据是返回的反序列化函数名
我么可以把内置的copyreg模块同pickle、结合起来使用,以便为旧数据添加缺失的属性,进行类的版本管理,并给序列化之后的数据提供固定的引入路径。
第45条:应该用datetime模块来处理本地时间,而不是用time模块
协调世界时间(Coordinated Universal Time, UTC)
from datetime import datetime, timezone
now = datetime(2010, 8 , 10, 18, 19,30)
now_utc = now.replace(tzinfo=timezone.utc)
now_local = now_utc.astimezone()
time_str = '2010-8-10 11:18:30'
now = datetime.strptime(time_str, time_format)
time_tuple = now.timetuple()
utc_now = mktime(time_tuple)
我们只能通过datetime中的tzinfo类及相关方法,来使用这套时区操作机制,datetime并没有提供UTC之外的时区定义。
pytz模块是一个第三方库,提供了时区功能
为了使用pytz模块,应该把当地时间转换为UTC,针对UTC进行datetime操作,最后再把UTC转换成当地时间。有点像字符的Unicode
第46条:使用内置算法和数据结构
双向队列collections.deque
从list头部插入或删除元素,会耗费O(n),而deque操作是O(1)
有序字典collecitons.OrderDict
带有默认值的字典collecitons.defaultdict
堆队列(优先级队列)heapq.heap,用来实现优先级队列
heappush, heappop, nsmallest等函数
元素会按照优先级从高到低顺序从堆中弹出,数值小的元素优先级高。
a = []
heappush(a, 5)
heappush(a, 3)
assert a[0] == nsmallest(1, a)[0] == 3 用a[0]可以获取最小值
heap操作时间复杂度与列表长度对数成正比O(logN)
二分查找bisect模块有bisect_left等函数,提供二分折半搜索算法
i = bisect_left(x, 99999)
与迭代器有关的工具:itertools模块包含大量的迭代函数
第47条:在重视精度场合,应该使用decimal
内置的decimal模块的Decimal类可以解决小数精度问题,可以进行定点数学运算
如果要用精度不受限制的方式来表达有理数,那么可以考虑使用Fraction类,该类在内置的fractions模块中。
第48条:学会安装由python开发者社区所构建的模块
pip3,pip命令
第49条:为每个函数、类和模块编写文档字符串
也要为每个模块编写文档描述符
第50条:用包来安排模块,并提供稳固的API
包:包含其他模块的模块
大多数情况,我们会给目录放入__init__.py文件,用这个方式来定义包
只要有__init__.py,我们就可以用相对于该目录的路径,来引入目录中的其他python文件
包有两大用途:
1、名称空间:包可以把模块划分到不同的名称空间中,开发者可以编写多个文件名相同的模块,并放在不同的绝对路径下
如果重名文件导入同时导入,可以用as语句重新命名
2、稳固的API
把模块的内部实现隐藏起来,对外提供稳定的接口。
在模块中定义__all__属性,比如下面:
all = [‘Projectile’]
这样当使用import *导入时只会导入__all__中指定的接口。
如果没有__all__,import *会只导入public属性,任何以_开头方法属性都不会导出
在package的__init__中定义所有API接口的方法:
#init.py
all = []
from . models import *
all += models.all
from . utils import *
all += utils.all
另外,注意谨慎使用import *,而应该用form x import y明确指定导入模块
只要把__init__.py放入含有其他源文件的目录里,就可以将该目录定位为包。目录中文件都会成为包的子模块。
包下面也可以包含其他包
第51条:为自编的模块定义根异常,以便将调用者和API相隔离
根异常:class Error(Exception):
class InvalidDensityError(Error):
except mu_module.InvalidDensityError:
好处:
1、便于调试
2、便于后续演化
第52条:用适当的方式打破循环依赖关系
引入模块时,python会按照深度优先的顺序执行下列操作:
1、在由sys.path所指定的路径中,搜寻待引入的模块
2、从模块中加载代码,并保证这段代码能够正确编译
3、创建于该模块相对应的空对象
4、把这个空模块对象,添加到sys.modules里面
5、运行模块对象中的代码,以定义其内容。
解决循环依赖的方法:
1、最好的方式是重构代码,把相同部分提取出来放到单独模块中;
2、动态引入,在需要的时候在import模块,如果是循环执行的话,可能会反复引入,而且也违背pytonPEP08指南
第53条:用虚拟环境隔离项目,并重建其依赖关系
pip3 show module 可以显示模块信息,和依赖关系
pip install --upgrade升级包
从python3.4开始,pyvenv工具解决多套虚拟环境问题
python -m venv访问虚拟环境
激活虚拟环境:source bin/active
active会自己配置环境
新创建的虚拟环境只有pip和setuptools,其他模块都没有安装
deactive可以去激活虚拟环境
pip3 freeze > requirements.txt,导出环境信息
pip3 install -r /tmp/myproject/requirements.txt 导入虚拟环境并安装
第54条:考虑用模块级别的代码来配置不同的部署环境
比如开发环境、生产环境等,需要不同的部署方式
可以通过宏来控制,比如:
主模块main.py中定义TESTING宏等于True或False
数据库模块中根据import main if main.TESTTING的值来连接不同的数据库
还有我们可以通过configparse模块来用配置文件方式解决不同环境配置问题,将代码和配置分离
、
还可以通过操作系统来区分,比如sys.platform.startswithc('win32')
第55条:通过repr字符串来输出调试信息
repr可以区分变量类型,repr(5)与repr(‘5’)输出结果是不同的,通过%r也可以输出repr类型
repr对应类的內建方法就是__repr__方法、这样也能用print(obj)来打印__repr__内容
第56条:用unittest来测试全部代码
python没有静态类型检查机制。
只有通过编写测试,我们才能够确信程序在运行的时候不会出现问题。、
举例:
from unittest import Testcase, main
from utils import to_str
class UtillsTestCase(TestCase):
def test_to_str_bytes(self):
self.assertequal('hello', to_str(b'hello'))
def test_to_str_str(sef):
self.assertEqual('hello', to_str('hello'))
注意:
1、测试必须要TestCase类进行
2、每个case必须以test开始
3、如果需要模拟器,需要用mock函数或modek类
4、继承TestCase的子类如果实现了setUp方法或者tearDown函数,那么每个测试case开始或结束时都会执行这两个函数
第57条:考虑用pdb实现交互调试
在需要调试的代码里面加入下面语句:
import pdb; pdb.set_trace()
这样运行到这里会进入pdb交互调试环境
下面是一些调试命令:
1、locals可以打印本地所有局部变量
2、bt,打印调用栈
3、up,沿着调用栈上溯一层
4、down,眼调用栈下移一层
5、step
6、next
7、return, 到返回地方停止
8、continue,go运行
第58条:先分析性能,然后优化
python提供了内置的性能分析工具profiler,一种是纯Python的,一种是C语言扩展的叫cProfile,通常用后者,因为开销少
启动:
profiler = Profile()
profiler.runcall(test)
执行完后,调用下面进行分析:
stats = Stats(profiler)
stats.strip_dirs()
stats.sort_stats(‘cumulative’)
stats.print_state()
stats.print_callers() 打印调用者
执行结果:
ncalls:调用次数
tottime: 总花费时间,调用子函数不算
cumtime, 执行总花费时间,包含子函数
percall,每次调用花费时间,分是否包含子函数
第59条:用tracemalloc来掌握内存的使用及泄露情况
调试内存有两个方法,一个是gc,一个是tracemalloc
gc举例:
import gc
found_objects = gc.get_objects()
…
foudn_objects_end = gc.get_objects()
print("%d objects after’ % len(foudn_objects_end)
gc缺点是不会告诉我们这些对象是如何分配出来的
从python3.4开始引入tracemalloc
import tracemalloc
tracemalloc.start(10)
time1 = tracemalloc.take_snapshot()
....
time2 = tracemalloc.take_snapshot()
stats = time2.compare_to(time1 'lineno')
stats = time2.compare_to(time1 'tracebakc')
top = state[0]
print('\n'.join(top.traceback.formant())) #可以打印调用栈
tracemalloc可以打印出每条内存分配的完整堆栈信息,打印的最大帧数量有start的参数决定,如上面是10层调用栈