Effective python
第一章:用pythonic的方式思考
1:确认使用的python版本
python --version
python3 --version
2:遵循PEP8风格指南
《Python Enhancement Proposal #8》又叫PEP8,它是针对Python代码风格而编订的风格指南。链接
采用一致的代码风格来书写可以令代码更加易懂、更加易读;
3:了解bytes、str与Unicode的区别
python2有两种表示字符的类型:str、Unicode;str包含原始的8位值,Unicode包含Unicode字符。
python3有两种表示字符的类型:bytes、str;bytes包含原始的8位值,str包含Unicode字符。
在编写程序时,一定要把编码、解码的操作放在界面最外围来做,程序的核心应该使用Unicode字符类型。
注意:
- python2中,如果str只包含7位ASCII字符,此时Unicode和str似乎就成了同一种类型,可以使用+、等价、不等价来判断str和Unicode,也可以使用%s来格式化Unicode。
- python3中,内置open函数默认会以utf-8编码格式来操作文件,python2中默认是二进制格式。
4:用辅助函数来取代复杂的表达式
总结下来就是:
- 开发者很容易过度运用python的语法特性,从而写出那种特别复杂又难以理解的单行表达式。
- 请把复杂的表达式移到辅助函数中,如果要反复使用相同的逻辑,那更应该这么做。
- 使用if/else表达式,要比用or、and这样的bool操作符写成的表达式更加清晰。
5:了解切割序列的办法
- 不要写多余的代码:当start索引为0,或者end索引为序列长度时,应该将其省略。
- 切片操作不会计较start与end索引是否越界,这使得我们很容易就能从序列的前端或者后端开始,对其进行范围固定的切片操作。
- 对list赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换。
6:在单次切片操作内,不要同时指定start、end和stride
- 既有start和end,又有stride的切割操作,可能会令人费解。
- 尽量使用stride为正数,且不带start或end索引的切割操作。尽量避免使用负数做stride。
- 在同一个切片操作内,不要同时使用start、end和stride。
7:用列表推导来取代map和filter
- 列表推导要比内置的map和filter函数清晰,因为它无需额外编写lambda表达式。
- 列表推导可以跳过输入列表中的某些元素,如果改用map来做,那就必须辅以filter方能实现。
- 字典和集合也支持推导表达式。
8:不要使用包含两个以上表达式的列表推导
- 超过两个表达式的列表推导是很难理解的,应该尽量避免。
9:用生成器表达式来改写数据量较大的列表推导
- 当输入的数据量较大时,列表推导可能会因为占用太多内存而出问题。
- 由生成器表达式返回的的迭代器,可以逐次产生输出值,从而避免了内存占用高的问题。
10:尽量用enumerate取代range
- enumerate函数提供了一种简洁的写法,可以在遍历迭代器时获知每个元素的索引。
- 尽量用enumerate来改写那种将range与下标访问相结合的序列遍历代码。
- 可以给enumerate提供第二个参数,以指定开始计数时所用的值(默认为0)。
11:用zip函数同时遍历两个迭代器
12:不要在for和while循环后面写else块
- 只有当整个循环主体都没有遇到break语句时,循环后面的else块才会执行。
- 不要在循环后面使用else块,因为这种写法既不直观,又容易引人误解。
13:合理利用try/except/else/finally结构中的每个代码块
- 无论try块是否发生异常,都可利用try/finally复合语句中的finally块来执行清理工作。
- else块可以用来缩减try块中的代码量,并把没有发生异常时所要执行的语句与try/except代码块隔开。
- 顺利执行try块后,若想使某些操作能在finally块的清理代码之前执行,则可以将这些操作写到else块中。
第二章:函数
14:尽量用异常来表示特殊情况,而不要返回None
- 用None这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为None和0以及空字符串之类的值,在条件表达式中都会评估为False。
- 函数在遇到特殊情况时应该抛出异常,而不是返回None。
15:了解如何在闭包中使用外围作用域中的变量
去了解global和nonlocal的用法(nonlocal是python3的语法)
- 对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量,但是不能修改不可变变量。
- 使用默认方式对闭包内的变量赋值不会影响外围作用域中的变量值。
- python3可以使用nonlocal修饰某个名称,使该闭包能够修改外围作用域中的同名变量。
- python2可以使用可变值来实现nonlocal相仿的机制(放入列表中)。
16:考虑用生成器来改写直接返回表聊得函数。
- 当数据量大时,使用迭代器替换直接返回列表可以避免消耗太大的内存。
17:在参数上面迭代时,要多加小心
- 函数在参数上多次迭代要小心,如果参数是迭代器,那么可能会导致奇怪的行为并错失某些值。
- python的迭代器协议,描述了容器和迭代器应该如何与iter和next内置函数、for循环及相关表达式配合。
- 把
__iter__
实现为生成器,即可定义自己的容器。 - 判断某个值是不是迭代器,可以两次调用
iter
方法,如果两次值相同,就是迭代器,可以调用next方法使迭代器前进一步。
for x in foo会先调用iter(foo)返回一个迭代器对象,iter会调用foo.__iter__
方法。
18:用数量可变的位置参数减少视觉杂讯
- def定义函数时使用*arg,可令函数接受数量可变的位置参数。
19:用关键字参数表达可选的行为
- 函数参数可以按位置或关键字来指定。
- 只使用位置参数可能会使函数调用不明晰。
- 给函数添加新行为时,可以使用带默认值的关键字参数,以保持代码兼容。
- 可选参数应该总是以关键字参数形式出现,而不是可变位置参数。
20:用None和文档字符串来描述具有动态默认值的参数
- 参数的默认值只会在加载模块时计算一次,对于{},[]等动态的值,可能会出现奇怪的问题。
- 应该把默认值设置为None,在函数中判断为None的时候再设置为需要的值。
21:用只能以关键字形式指定的参数来确保代码明晰
- 关键字参数能够使函数调用的意图更加明确。
第三章:类与继承
22:尽量用辅助类来维护程序的状态,不要用元组或者字典
- 不要使用包含其他字典的字段,也不要使用过长的元组。
- 如果容器包含简单不可变的数据,可以考虑使用nametuple,但是nametuple也有自己的局限。
- 保存内部状态的字典如果变得比较复杂,就应该把这些代码拆解为多个辅助类。
23:简单的接口应该接收函数,而不是类的实例
- python中的函数和对象一样,都是都可以当做参数传递给函数的。
- 通过名为
__call__
的特殊方法,可以使类的实例能够像普通的python函数那样得到调用。 - 如果要用函数保存状态,那就定义新的类,并实现
__call__
方法,而不要定义带状态的闭包。
24:以@classmethod形式的多态去通用的构建对象
- 在python中,每个类只能有一个构造器,也就是
__init__
方法。 - 通过@classmethod机制,可以用一种与构造器相仿的方式来构造类的对象。
- 通过类方法多态机制,能够以更加通用的方式构建并拼接具体的子类。
25:用super初始化父类
- python采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题,可以用mro方法来查看这个顺序,调用顺序和这个顺序相反。
- 总是应该使用内置的super函数来初始化父类。
26:只在使用Mix-in组件制作工具时进行多重继承
mix-in是一种小型的类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的实例属性,也不要求使用者调用自己的__init__
方法。
- 能用mix-in组件实现的效果,就不要用多重继承来做。
- 将各个功能实现为可插拔的mix-in组件,使需要该功能的类继承mix-in组件即可具备该行为。
27:多用public属性,少用private属性
- python编译器无法严格保证private字段的私密性。
- 不要盲目的将属性设置为private。
- 应该多用protected,即以一个下划线为前缀的属性名。
28:继承collections.abc以实现自定义的容器类型
collections.abc模块定义了一系列抽象基类,它们提供了每一种容器类型所应该具备的常用方法。从这样的基类中继承之类之后,如果忘记实现某个方法,那么collections.abc模块就会指出这个错误。
- 编写自定义的容器类型时,可以从collections.abc模块的抽象基类中继承,那些基类能够确保我们的之类拥有适当的接口和行为。
第四章:元类及属性
29:用纯属性取代get和set方法
- 编写新类时,应该用最简单的public属性来定义其接口。
- 如果要控制某个属性的访问时,应该是用@property。
- @property修饰的方法应该尽量简单。
30:考虑用@property来代替属性重构
- @property可以为现有的实例属性添加新的功能
- 可以用@property来逐步完善数据模型
31:用描述符来改写需要复用的@property方法
- 如果想复用@property方法及其验证机制,可以自己定义描述符类。
- WeakKeyDictionary可以保证描述符类不会内存泄露。
32:用__getattr__
和__get_attribute__
和__setattr__
实现按需生成的属性。
如果某个类定义了__getattr__
,同时系统在该类对象的实例字典中又找不到待查询的属性,系统就会调用该方法。
- 通过
__setattr__
和__getattr__
,我们可以用惰性的方式来加载并保存对象的属性。 - 要理解
__getattr__
和__getattribute__
的区别:前者只会在属性不存在时才会被调用,而后者则会在每次访问属性时触发。 - 如果要在
__getattribute__
和__setattr__
方法中访问实例属性,应该直接通过super()
来做,避免无限递归。
33:用元类来验证子类
元类最简单的一种用途,就是验证某个类定义的是否正确。构建复杂的类体系时,我们可能需要确保类的风格协调一致、确保某些方法得到了覆写,或是确保类属性之间具备某些严格的关系。
- 通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范。
34:用元类来注册子类
- 在构建模块化的python程序时,类的注册是一种很有用的模式。
- 通过元类来实现类的注册,可以确保所有的子类都不会被遗漏。
35:用元类来注解类的属性
- 借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性。
第五章:并发及并行
36:用subprocess模块管理子进程
- 可以用subprocess模块运行子进程,并管理其输入流与输出流。
37:可以用线程来执行阻塞式io,但不要用它做平行计算
- python的多线程适合io密集型应用。
38:在线程中使用lock来防止数据竞争
- python虽然存在GIL,但是多线程依然需要加锁保护竞争资源。
39:用Queue来协调各线程之间的工作
40:考虑用协程来并发地运行多个函数。
虽然可以用开发多个线程来实现并发,但是线程有以下缺点:
- 为了确保数据安全,必须要使用同步工具。
- 线程需要占用大量内存,大约8M。
- 线程启动时的开销比较大。
- 协程提供了一种有效的方式,令程序看上去好像能够同时运行大量函数。
41:考虑用concurrent.futures来实现真正的平行计算
- 利用ProcessPoolExecutor可以突破GIL的限制,利用多核的优势,但是这种方案的限制是仅适用于需要在进程间传递少量数据的场景。
第六章:内置模块
42:用functools.wraps定义函数装饰器
- python为装饰器提供了专门的语法,它使得程序在运行的时候,能够用一个函数来修改另一个函数。
- 内置的functools模块提供了名为wraps的装饰器,开发者在定义自己的装饰器时应该用wraps对其做一些处理,避免一些问题。
43:考虑以contextlib和with语句来改写可复用的try/finally代码
- 内置模块contextlib提供了contextmanager的装饰器,方便开发者用来实现自己的上下文管理。
44:用copyreg实现可靠的pickle操作
- 内置的pickle模块,只适合用来在彼此信任的程序之间,对相关对象执行序列化和反序列化。
- 把内置的copyreg和pickle结合起来使用,以便给旧数据添加缺失的属性值、进行类的版本管理。
45:应该用datetime模块来处理本地时间,而不是用time
time模块的实现依赖操作系统而运行,应该仅仅用来在UTC(时间戳)和当地时区时间之间转换。
- 不要用time模块在不同时区之间进行转换。
- 如果要在不同时区之间,可靠地执行转换操作,那就应该使用datetime和pytz。
- 开发者应该总是把时间转换为UTC格式,然后做各种操作,最终转换为当地时间。
46:使用内置算法与数据结构
双向队列:deque
有序字典:OrderedDict()
带有默认值的字典:defaultdict()
堆队列:heap
与迭代有关的工具:itertools
47:在重视精确度的场合,应该使用decimal
48:学会安装有python开发者社区所构建的模块
第七章:协作开发
49:为每个函数、类和模块编写文档字符串
50:用包来安排模块,并提供稳固的API
51:为自编的模块定义根异常,以便调用者与API相隔离
52:用适当的方式打破循环依赖关系
53:用虚拟环境隔离项目,并重建其依赖关系
第八章:部署
54:考虑用模块级别的代码来配置不同的部署环境
55:通过repr字符串来输出调试信息
- repr可以展示要输出的对象的类型。
56:用unittest来测试全部代码
57:考虑用pdb实现交互调试
import pdb
pdb.settrace()
58:先分析性能再优化
- Profile和CProfile工具可以用来分析性能。
59:用tracemalloc来掌握内存的使用及泄露情况
- python内置的gc
- python3.4支持的tracemalloc
- python2可以使用开源包:heapy