0%

《Effective Python 编写高质量Python代码的59个有效方法》读书笔记

《Effective Python 编写高质量Python代码的59个有效方法》读书笔记,已完结。

本书基于 Python3.4,同一条目往往也同时包含了 Python2 版本的实现,在阅读时简单浏览或直接略过即可。

资源

本书电子版

本书官网

本书Github地址

目录

第 1 章 用 Pythonic 方式来思考

第 1 条:确认自己所用的 Python 版本

现在 Python 2 已经是历史了,这条可以忽略。

第 2 条:遵循 PEP8 风格指南

直接看官方文档就行:PEP 8 – Style Guide for Python Code

第 3 条:了解 bytes、str 与 unicode 的区别

这条不如廖雪峰解释的详细:字符串和编码

作者建议写两个 str 和 bytes 相互转换的通用方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value # Instance of str

def to_bytes(bytes_or_str):
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value # Instance of bytes

第 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 来掌握内存的使用及泄漏情况

这条一般场景也用不上。

觉得文章有帮助,打赏1元鼓励一下作者