《Effective Python 编写高质量Python代码的59个有效方法》读书笔记,已完结。
本书基于 Python3.4,同一条目往往也同时包含了 Python2 版本的实现,在阅读时简单浏览或直接略过即可。
资源
目录
第 1 章 用 Pythonic 方式来思考
第 1 条:确认自己所用的 Python 版本
现在 Python 2 已经是历史了,这条可以忽略。
第 2 条:遵循 PEP8 风格指南
直接看官方文档就行:PEP 8 – Style Guide for Python Code
第 3 条:了解 bytes、str 与 unicode 的区别
这条不如廖雪峰解释的详细:字符串和编码
作者建议写两个 str 和 bytes 相互转换的通用方法,如下所示:
1 | def to_str(bytes_or_str): |
第 4 条:用辅助函数来取代复杂的表达式
编写 Python 程序时,不要一味追求过于紧凑的写法。
第 5 条:了解切割序列的办法
P11
切割列表时,即便 start 或 end 索引越界也不会出问题。利用这一特性,我们可以限定输入序列的最大长度。
反之,访问列表中的单个元素时,下标不能越界,否则会导致异常。P12
对原列表进行切割之后,会产生另外一份全新的列表。系统依然维护着指向原列表中各个对象的引用。在切割后得到的新列表上进行修改,不会影响原列表。
P13
对 list 赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换。
第 6 条:在单次切片操作内,不要同时指定 start、end 和 stride
- P15
既有 start 和 end,又有 stride 的切割操作,可能会令人费解。
尽量使用 stride 为正数,且不带 start 或 end 索引的切割操作。尽量避免用负数做 stride。
在同一个切片操作内,不用同时使用 start、end 和 stride。如果确实需要执行这种操作,那就考虑将其拆解为两条赋值语句,其中一条做范围切割,另一条做步进切割,或考虑使用内置 itertools 模块中的 islice。
第 7 条:用列表推导来取代 map 和 filter
- P16
列表推导要比内置的 map 和 filter 函数清晰,因为它无需额外编写 lambda 表达式。
列表推导可以跳过输入列表中的某些元素,如果改用 map 来做,那就必须辅以 filter 方能实现。
字典与集也支持推导表达式。
第 8 条:不要使用含有两个以上表达式的列表推导
总之,列表推导的循环和条件语句不要写太复杂,否则,还不如用普通的 for 循环和 if 语句代替。
第 9 条:用生成器表达式来改写数据量较大的列表推导
这条廖雪峰教程写的更详细:生成器
第 10 条:尽量用 enumerate 取代 range
遍历序列时,如果需要使用下标(索引),就用 enumerate;如果不需要使用下标,就用 range。
第 11 条:用 zip 函数同时遍历两个迭代器
这条直接看官方文档就行:zip(*iterables, strict=False)
第 12 条:不要在 for 和 while 循环后面写 else 块
字面意思,Python 循环后的 else 块语法非常诡异,不要使用。
第 13 条:合理利用 try/except/else/finally 结构中的每个代码块
看官方文档就行:错误和异常
第 2 章 函数
本章第 18 条到第 21 条的内容,廖雪峰教程也概况的比较好:函数的参数
第 14 条:尽量用异常来表示特殊情况,而不要返回 None
- P30
用 None 这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为 None 和 0 及空字符串之类的值,在条件表达式里都会评估为 False。
函数在遇到特殊情况时,应该抛出异常,而不要返回 None。调用者看到该函数的文档中所描述的异常之后,应该就会编写相应的代码来处理它们了。
第 15 条:了解如何在闭包里使用外围作用域中的变量
- P34
对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量。
使用默认方式对闭包内的变量赋值,不会影响外围作用域中的同名变量。
在 Python 3 中,程序可以在闭包内用 nonlocal 语句来修饰某个名称,使该闭包能够修改外围作用域中的同名变量。
除了那种比较简单的函数,尽量不要用 nonlocal 语句。
第 16 条:考虑用生成器来改写直接返回列表的函数
- P37
使用生成器比把收集到的结果放入列表里返回给调用者更加清晰。
由生成器函数所返回的那个迭代器,可以把生成器函数体中,传给 yield 表达式的那些值,逐次产生出来。
无论输入量有多大,生成器都能产生一系列输出,因为这些输入量和输出量,都不会影响它在执行时所耗的内存。
第 17 条:在参数上面迭代时,要多加小心
- P41
函数在输入的参数上面多次迭代时要当心:如果参数是迭代器,那么可能导致奇怪的行为并错失某些值。
Python 的迭代器协议,描述了容器和迭代器应该如何与 iter 和 next 内置函数、for 循环及相关表达式相互配合。
把 __iter__ 方法实现为生成器,即可定义自己的容器类型。
想判断某个值是迭代器还是容器,可以拿该值为参数,两次调用 iter 函数,若结果相同,则是迭代器,调用内置的 next 函数,即可令该迭代器前进一步。
关于迭代器,可查看官方文档:迭代器类型
第 18 条:用数量可变的位置参数减少视觉杂讯
P42
变长参数在传给函数时,总是要先转化成元组(tuple)。
P43
在 def 语句中使用 *args,即可令函数接受数量可变的位置参数。
调用函数时,可以采用 * 操作符,把序列中的元素当成位置参数,传给该函数。
对生成器使用 * 操作符,可能导致程序耗尽内存并崩溃。
在已经接受 *args 参数的函数上面继续添加位置参数,可能会产生难以排查的 bug。
第 19 条:用关键字参数来表达可选的行为
- P46
函数参数可以按位置或关键字来指定。
只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键字参数则能够阐明每个参数的意图。
给函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数调用代码保持兼容。
可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式来指定。
第 20 条:用 None 和文档字符串来描述具有动态默认值的参数
- P49
参数的默认值,只会在程序加载模块并读到本地函数的定义时评估一次。对于 {} 或 [] 等动态的值,这可能会导致奇怪的行为。
对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为 None,并在函数的文档字符串里面描述该默认值所对应的实际行为。
第 21 条:用只能以关键字形式指定的参数来确保代码明晰
- P52
关键字参数能够使函数调用的意图更加明确。
对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接受多个 Boolean 标志的函数,更应该这样做。
在编写函数时,Python3 有明确的语法来定义这种只能以关键字形式指定的参数。
第 3 章 类与继承
第 22 条:尽量用辅助类来维护程序的状态,而不要用字典和元组
- P58
不要使用包含其他字典的字典,也不要使用过长的元组。
如果容器中包含简单而又不可变的数据,那么可以先使用 namedtuple 来表示,待稍后有需要时,再修改为完整的类。
保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类。
感觉本节所讲的规则比较明确,但举的例子反而比较生涩。
关于 namedtuple 的使用,可以看廖雪峰教程:collections
第 23 条:简单的接口应该接受函数,而不是类的实例
- P62
对于连接各种 Python 组件的简单接口来说,通常应该给其直接传入函数,而不是先定义某个类,然后再传入该类的实例。
Python 中的函数和方法都可以像一级类那样引用,因此,它们与其他类型的对象一样,也能够放在表达式里面。
通过名为 __call__ 的特殊方法,可以使类的实例能够像普通的 Python 函数那样得到调用。
如果要用函数来保存状态,那就应该定义新的类,并令其实现 __call__ 方法,而不要定义带状态的闭包。
第 24 条:以 @classmethod 形式的多态去通用地构建对象
- P66
在 Python 程序中,每个类只能有一个构造器,也就是 __init__ 方法。
通过 @classmethod 机制,可以用一种与构造器相仿的方式来构造类的对象。
通过类方法多态机制,我们能够以更加通用的方式来构建并拼接具体的子类。
讲真这条道理其实挺简单的,但是作者为了说明举了个比较复杂的例子。。
第 25 条:用 super 初始化父类
- P70
Python 采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
总是应该使用内置的 super 函数来初始化父类。
第 26 条:只在使用 Mix-in 组件制作工具类时进行多重继承
- P75
能用 mix-in 组件实现的效果,就不要用多重继承来做。
将各功能实现为可插拔的 mix-in 组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所具备的行为。
把简单的行为封装到 mix-in 组件里,然后就可以用多个 mix-in 组合出复杂的行为了。
这一条写的不错,值得再反复看。相比之下,廖雪峰讲的太简略了:多重继承
第 27 条:多用 public 属性,少用 private 属性
- P79
Python 编译器无法严格保证 private 字段的私密性。
不要盲目地将属性设为 private,而是应该从一开始就做好规划,并允许子类更多地访问超类的内部 API。
应该多用 protected 属性,并在文档中把这些字段的合理用法告诉子类的开发者,而不要试图用 private 属性来限制子类访问这些字段。
只有当子类不受自己控制时,才可以考虑用 private 属性来避免名称冲突。
第 28 条:继承 collections.abc 以实现自定义的容器类型
- P83
如果要定制的子类比较简单,那就可以直接从 Python 的容器类型(如 list 或 dict)中继承。
想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
编写自制的容器类型时,可以从 collections.abc 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为。
说实话我感觉在日常代码中,实现自定义容器类型,貌似除了增加代码理解难度之外,没有明显的优势。
书中举例可以用来实现二叉树的下标、长度等操作,倒是一个巧妙的方法,让我想起了《算法第4版》这本书。
第 4 章 元类及属性
第 29 条:用纯属性取代 get 和 set 方法
- P88
编写新类时,应该用简单的 public 属性来定义其接口,而不要手工实现 set 和 get 方法。
如果访问对象的某个属性时,需要表现出特殊的行为,那就用 @property 来定义这种行为。
@property 方法应该遵循最小惊讶原则,而不应产生奇怪的副作用。
@property 方法需要执行得迅速一些,缓慢或复杂的工作,应该放在普通的方法里面。
第 30 条:考虑用 @property 来代替属性重构
- P92
@property 可以为现有的实例属性添加新的功能。
可以用 @property 来逐步完善数据模型。
如果 @property 用得太过频繁,那就应该考虑彻底重构该类并修改相关的调用代码。
第 31 条:用描述符来改写需要复用的 @property 方法
- P96
如果想复用 @property 方法及其验证机制,那么可以自己定义描述符类。
WeakKeyDictionary 可以保证描述符类不会泄漏内存。
通过描述符协议来实现属性的获取和设置操作时,不要纠结于 __getattribute__ 的方法具体运作细节。
这条很古怪,作者举的例子也没有说服力,多门成绩编写同样的方法,不一定非要编写不同的类啊,不可以都继承自一个基类?
第 32 条:用 __getattr__、__getattribute__ 和 __setattr__ 实现按需生成的属性
- P101
通过 __getattr__ 和 __setattr__,我们可以用惰性的方式来加载并保存对象的属性。
要理解 __getattr__ 和 __getattribute__ 的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性时触发。
如果要在 __getattribute__ 和 __setattr__ 方法中访问实例属性,那么应该直接通过 super() (也就是 object 类的同名方法)来做,以避免无限递归。
第 33 条:用元类来验证子类
- P104
通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范。
Python 系统把子类的整个 class 语句体处理完毕之后,就会调用其元类的 __new__ 方法。
第 34 条:用元类来注册子类
- P108
在构建模块化的 Python 程序时,类的注册是一种很有用的模式。
开发者每次从基类中继承子类时,基类的元类都可以自动运行注册代码。
通过元类来实现类的注册,可以确保所有子类都不会遗漏,从而避免后续的错误。
第 35 条:用元类来注解类的属性
- P111
借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性。
描述符与元类能够有效地组合起来,以便对某种行为做出修饰,或在程序运行时探查相关信息。
如果把元类与描述符相结合,那就可以在不使用 weakref 模块的前提下避免内存泄漏。
第 5 章 并发及并行
第 36 条:用 subprocess 模块来管理子进程
- P116
可以用 subprocess 模块运行子进程,并管理其输入流与输出流。
Python 解释器能够平行地运行多条子进程,这使得开发者可以充分利用 CPU 的处理能力。
可以给 communicate 方法传入 timeout 参数,以避免子进程死锁或失去响应(hanging,挂起)。
讲真这条没太看懂,应该是在 Python 中调用其他程序时可以用到。
第 37 条:可以用线程来执行阻塞式 I/O,但不要用它做平行计算
- P121
因为受到全局解释器锁(GIL)的限制,所以多条 Python 线程不能在多个 CPU 核心上面平行地执行字节码。
尽管受制于 GIL,但是 Python 的多线程功能依然很有用,它可以轻松地模拟出同一时刻执行多项任务的效果。
通过 Python 线程,我们可以平行地执行多个系统调用,这使得程序能够在执行阻塞式 I/O 操作的同时,执行一些运算操作。
第 38 条:在线程中使用 Lock 来防止数据竞争
- P124
Python 确实有全局解释器锁,但是在编写自己的程序时,依然要设法防止多个线程争用同一份数据。
如果在不加锁的前提下,允许多条线程修改同一个对象,那么程序的数据结构可能会遭到破坏。
在 Python 内置的 threading 模块中,有个名叫 Lock 的类,它用标准的方式实现了互斥锁。
第 39 条:用 Queue 来协调各线程之间的工作
- P131
管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条 Python 线程来同时执行这些任务。
构建并发式的管线时,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程,以及如何防止内存膨胀等。
Queue 类所提供的机制,可以彻底解决上述问题,它具备阻塞式的队列操作、能够指定缓冲区尺寸,而且还支持 join 方法,这使得开发者可以构建出健壮的管线。
这条只看了大概,用到时再查吧。
第 40 条:考虑用协程来并发地运行多个函数
- P140
协程提供了一种有效的方式,令程序看上去好像能够同时运行大量函数。
对于生成器内的 yield 表达式来说,外部代码通过 send 方法传给生成器的那个值,就是该表达式所要具备的值。
协程是一种强大的工具,它可以把程序的核心逻辑,与程序同外部环境交互时所用的代码相分离。
第 41 条:考虑用 concurrent.futures 来实现真正的平行计算
- P144
把引发 CPU 性能瓶颈的那部分代码,用 C 语言扩展模块来改写,即可在尽量发挥 Python 特性的前提下,有效提升程序的执行速度。但是,这样做的工作量比较大,而且可能会引入 bug。
multiprocessing 模块提供了一些强大的工具。对于某些类型的任务来说,开发者只需编写少量代码,即可实现平行计算。
若想利用强大的 multiprocessing 模块,最恰当的做法,就是通过内置的 concurrent.futures 模块及其 ProcessPoolExecutor 类来使用它。
multiprocessing 模块所提供的那些高级功能,都特别复杂,所以开发者尽量不要直接使用它们。
第 6 章 内置模块
第 42 条:用 functools.wraps 定义函数修饰器
- P147
Python 为修饰器提供了专门的语法,它使得程序在运行的时候,能够用一个函数来修改另一个函数。
对于调试器这种依靠内省机制的工具,直接编写修饰器会引发奇怪的行为。
内置的 functools 模块提供了名为 wraps 的修饰器,开发者在定义自己的修饰器时,应该用 wraps 对其做一些处理,以避免一些问题。
廖雪峰写的也比较通俗易懂:装饰器
第 43 条:考虑以 contextlib 和 with 语句来改写可复用的 try/finally 代码
- P151
可以用 with 语句来改写 try/finally 块中的逻辑,以便提升复用程度,并使代码更加整洁。
内置的 contextlib 模块提供了名叫 contextmanager 的修饰器,开发者只需用它来修饰自己的函数,即可令该函数支持 with 语句。
情境管理器可以通过 yield 语句向 with 语句返回一个值,此值会赋给由 as 关键字所指定的变量。该机制阐明了这个特殊情境的编写动机,并令 with 块中的语句能够直接访问这个目标变量。
这条配合廖雪峰教程食用更佳:contextlib
第 44 条:用 copyreg 实现可靠的 pickle 操作
- P157
内置的 pickle 模块,只适合用来在彼此信任的程序之间,对相关对象执行序列化和反序列化操作。
如果用法比较复杂,那么 pickle 模块的功能也许就会出问题。
我们可以把内置的 copyreg 模块同 pickle 结合起来使用,以便为旧数据添加缺失的属性值、进行类的版本管理,并给序列化之后的数据提供固定的引入路径。
第 45 条:应该用 datetime 模块来处理本地时间,而不是用 time 模块
- P161
不要用 time 模块在不同时区之间进行转换。
如果要在不同时区之间,可靠地执行转换操作,那就应该把内置的 datetime 模块与开发者社区提供的 pytz 模块搭配起来使用。
开发者总是应该先把时间表示成 UTC 格式,然后对其执行各种转换操作,最后再把它转回本地时间。
本条将时间与时区的转换,时区转换一般业务用不到,可以参考廖雪峰教程:datetime
第 46 条:使用内置算法与数据结构
本条介绍了 collections 模块中常见的数据结构、bisect 模块中的二分查找, 以及 itertools 模块中与迭代相关函数。有了这些,我们不必重复造轮子。
廖雪峰也简单介绍了这些,但不如书中全面:
第 47 条:在重视精确度的场合,应该使用 decimal
- P168
对于编程中可能用到的每一种数值,我们都可以拿对应的 Python 内置类型,或内置模块中的类表示。
Decimal 类非常适合用在那种对精度要求很高,且对舍入行为要求很严的场合,例如,设计货币计算的场合。
本条可以参考本博客的另一篇文章:Python 四舍五入用 round() ?
第 48 条:学会安装由 Python 开发者社区所构建的模块
这条讲用 pip 安装第三方包,现在已经是常识了,可以留意 Python 3.4 以后的版本才默认装有的 pip。
第 7 章 协作开发
第 49 条:为每个函数、类和模块编写文档字符串
本条讲了 Python 编写文档的规范。
第 50 条:用包来安排模块,并提供稳固的 API
- P179
把外界可见的名称,列在名为 __all__ 的特殊属性里,即可为包提供一套明确的 API。
如果想隐藏某个包的内部实现,那么我们可以在包的 __init__.py 文件中,只把外界可见的那些名称引入进来,或是给仅限内部使用的那些名称添加下划线前缀。
如果软件包只在开发团队或代码库内部使用,那可能没有必要通过 __all__ 来明确地导出 API。
第 51 条:为自编的模块定义根异常,以便将调用者与 API 相隔离
本条讲了定义自己的根异常和其子异常类有哪些好处。
第 52 条:用适当的方式打破循环依赖关系
- P187
如果两个模块必须相互调用对方,才能完成引入操作,那就会出现循环依赖现象,这可能导致程序在启动的时候崩溃。
打破循环依赖关系的最佳方案,是把导致两个模块相互依赖的那部分代码,重构为单独的模块,并把它放在依赖树的底部。
打破循环依赖关系的最简方案,是执行动态的模块引入操作,这样既可以缩减重构所花的精力,也可以尽量降低代码的复杂度。
还是按最佳方案来吧,动态模块引入可能会导致多次 import,产生莫名其妙的异常。
第 53 条:用虚拟环境隔离项目,并重建其依赖关系
个人总结:
conda 是隔离环境的,不限于 Python。
venv 是 Python3.3 之后有的:venv
pyenv 是切换不同 Python 版本的,在 Python3.6已被弃用:venv
virtualenv 是第三方库,和 venv 类似,不过支持 Python2.x。
第 8 章 部署
第 54 条:考虑用模块级别的代码来配置不同的部署环境
这条是讲可以根据不同部署环境写不同的代码。我感觉不如不同环境使用不同的配置合适。
第 55 条:通过 repr 字符串来输出调试信息
- P198
在格式化字符串里使用 %s,能够产生与 str 函数的返回值相仿的易读字符串,而在格式化字符串里使用 %r,则能够产生与 repr 函数的返回值相仿的可打印字符串。
第 56 条:用 unittest 来测试全部代码
本条讲了 Python 单元测试的方法。
第 57 条:考虑用 pdb 实现交互调试
讲真,在代码里写断点,手打命令,不如直接打断点好用。
第 58 条:先分析性能,然后再优化
本条讲 Python 中分析代码运行时间的工具。
第 59 条:用 tracemalloc 来掌握内存的使用及泄漏情况
这条一般场景也用不上。