具体的59条
第 1 章 用 Pythonic 方式来思考 1
第 1 条:确认自己所用的 Python 版本 1
第 5 条:了解切割序列的办法 10
第 6 条:在单次切片操作内,不要同时指定 start、 end 和 stride 13
第 7 条:用列表推导来取代 map 和 f ilter 15
第 8 条:不要使用含有两个以上表达式的列表推导 16
第 9 条:用生成器表达式来改写数据量较大的列表推导 18
第 10 条:尽量用 enumerate 取代 range 20
第 11 条:用 zip 函数同时遍历两个迭代器 21
第 12 条:不要在 for 和 while 循环后面写 else 块 23
第 13 条:合理利用 try/except/else/f inally 结构中的每个代码块 25
第 2 章 函数 28
第 14 条:尽量用异常来表示特殊情况,而不要返回 None 28
第 15 条:了解如何在闭包里使用外围作用域中的变量 30
第 16 条:考虑用生成器来改写直接返回列表的函数 35
第 17 条:在参数上面迭代时,要多加小心 37
第 18 条:用数量可变的位置参数减少视觉杂讯 41
第 19 条:用关键字参数来表达可选的行为 43
第 20 条:用 None 和文档字符串来描述具有动态默认值的参数 46
第 21 条:用只能以关键字形式指定的参数来确保代码明晰 49
第 3 章 类与继承 53
第 22 条:尽量用辅助类来维护程序的状态,而不要用字典和元组 53
第 23 条:简单的接口应该接受函数,而不是类的实例 58
第 24 条:以 @classmethod 形式的多态去通用地构建对象 62
第 25 条:用 super 初始化父类 67
第 26 条:只在使用 Mix-in 组件制作工具类时进行多重继承 71
第 27 条:多用 public 属性,少用 private 属性 75
第 28 条:继承 collections.abc 以实现自定义的容器类型 79
第 4 章 元类及属性 84
第 29 条:用纯属性取代 get 和 set 方法 84
第 30 条:考虑用 @property 来代替属性重构 88
第 31 条:用描述符来改写需要复用的 @property 方法 92
第 32 条:用 __getattr__、 __getattribute__ 和 __setattr__ 实现按需生成的属性 97
第 33 条:用元类来验证子类 102
第 34 条:用元类来注册子类 104
第 35 条:用元类来注解类的属性 108
第 5 章 并发及并行 112
第 36 条:用 subprocess 模块来管理子进程 113
第 37 条:可以用线程来执行阻塞式 I/O,但不要用它做平行计算 117
第 38 条:在线程中使用 Lock 来防止数据竞争 121XIV
第 39 条:用 Queue 来协调各线程之间的工作 124
第 40 条:考虑用协程来并发地运行多个函数 131
第 41 条:考虑用 concurrent.futures 来实现真正的平行计算 141
第 6 章 内置模块 145
第 42 条:用 functools.wraps 定义函数修饰器 145
第 43 条:考虑以 contextlib 和 with 语句来改写可复用的 try/f inally 代码 148
第 44 条:用 copyreg 实现可靠的 pickle 操作 151
第 45 条:应该用 datetime 模块来处理本地时间,而不是用 time 模块 157
第 46 条:使用内置算法与数据结构 161
第 47 条:在重视精确度的场合,应该使用 decimal 166
第 48 条:学会安装由 Python 开发者社区所构建的模块 168
第 7 章 协作开发 170
第 49 条:为每个函数、类和模块编写文档字符串 170
第 50 条:用包来安排模块,并提供稳固的 API 174
第 51 条:为自编的模块定义根异常,以便将调用者与 API 相隔离 179
第 52 条:用适当的方式打破循环依赖关系 182
第 53 条:用虚拟环境隔离项目,并重建其依赖关系 187
第 8 章 部署 193
第 54 条:考虑用模块级别的代码来配置不同的部署环境 193
第 55 条:通过 repr 字符串来输出调试信息 195
第 56 条:用 unittest 来测试全部代码 198
第 57 条:考虑用 pdb 实现交互调试 201
第 58 条:先分析性能,然后再优化 203
第 59 条:用 tracemalloc 来掌握内存的使用及泄漏情况 208
第 1 条:确认自己所用的 Python 版本 1
1.有两个版本的 Python 处于活跃状态,它们是: Python 2 与 Python 3。
2.有很多种流行的 Python 运行时环境,例如, CPython、 Jython、 IronPython 以及
PyPy 等。
3.在操作系统的命令行中运行 Python 时,请确保该 Python 的版本与你想使用的
Python 版本相符。
4.由于 Python 社区把开发重点放在 Python 3 上,所以在开发后续项目时,应该优
先考虑采用 Python 3。
第 2 条:遵循 PEP 8 风格指南 3
空白: Python 中的空白(whitespace)会影响代码的含义。 Python 程序员使用空白
的时候尤其在意,因为它们还会影响代码的清晰程度。
‰‰ 使用 space(空格)来表示缩进,而不要用 tab(制表符)。
‰‰ 和语法相关的每一层缩进都用 4 个空格来表示。
‰‰ 每行的字符数不应超过 79。
‰‰ 对于占据多行的长表达式来说,除了首行之外的其余各行都应该在通常的缩进
级别之上再加 4 个空格。
‰‰ 文件中的函数与类之间应该用两个空行隔开。
‰‰ 在同一个类中,各方法之间应该用一个空行隔开。
‰‰ 在使用下标来获取列表元素、调用函数或给关键字参数赋值的时候,不要在两
旁添加空格。
‰‰ 为变量赋值的时候,赋值符号的左侧和右侧应该各自写上一个空格,而且只写
一个就好。
命名: PEP 8 提倡采用不同的命名风格来编写 Python 代码中的各个部分,以便在阅
读代码时可以根据这些名称看出它们在 Python 语言中的角色。
‰‰ 函数、变量及属性应该用小写字母来拼写,各单词之间以下划线相连,例如,
lowercase_underscore
‰‰ 受保护的实例属性,应该以单个下划线开头,例如,_leading_underscore。
‰‰ 私有的实例属性,应该以两个下划线开头,例如,__double_leading_underscore。
‰‰ 类与异常,应该以每个单词首字母均大写的形式来命名,例如,CapitalizedWord。
‰‰ 模块级别的常量,应该全部采用大写字母来拼写,各单词之间以下划线相连,
例如, ALL_CAPS。
‰‰ 类中的实例方法( instance method),应该把首个参数命名为self,以表示该对象自身。
‰‰ 类方法( class method)的首个参数,应该命名为cls,以表示该类自身。
表达式和语句:《The Zen of Python》(Python之禅)中说:“每件事都应该有直白的做
法,而且最好只有一种。” PEP 8 在制定表达式和语句的风格时,就试着体现了这种思想。
‰‰ 采用内联形式的否定词,而不要把否定词放在整个表达式的前面,例如,应该
写 if a is not b 而不是if not a is b。
‰‰ 不要通过检测长度的办法(如 if len(somelist) == 0)来判断somelist是否为 []
或 '' 等空值,而是应该采用if not somelist这种写法来判断,它会假定:空值将
自动评估为 False。
‰‰ 检测 somelist是否为[1] 或'hi'等非空值时,也应如此, if somelist语句默认会把
非空的值判断为 True。
‰‰ 不要编写单行的 if 语句、 for循环、while 循环及 except 复合语句,而是应该把
这些语句分成多行来书写,以示清晰。
‰‰ import 语句应该总是放在文件开头。
‰‰ 引入模块的时候,总是应该使用绝对名称,而不应该根据当前模块的路径来
使用相对名称。例如,引入 bar 包中的foo模块时,应该完整地写出 from bar
import foo,而不应该简写为 import foo。
‰‰ 如果一定要以相对名称来编写 import 语句,那就采用明确的写法: from.import foo。
‰‰ 文件中的那些 import 语句应该按顺序划分成三个部分,分别表示标准库模块、
第三方模块以及自用模块。在每一部分之中,各 import 语句应该按模块的字母
顺序来排列
要点
‰‰ 当编写 Python代码时,总是应该遵循PEP 8 风格指南。
‰‰ 与广大 Python开发者采用同一套代码风格,可以使项目更利于多人协作。
‰‰ 采用一致的风格来编写代码,可以令后续的修改工作变得更为容易。
第 3 条:了解 bytes、 str 与 unicode 的区别
‰‰ 在 Python 3 中, bytes 是一种包含 8 位值的序列, str 是一种包含 Unicode 字符的
序列。开发者不能以 > 或 + 等操作符来混同操作 bytes 和 str 实例。
‰‰ 在 Python 2 中, str 是一种包含 8 位值的序列, unicode 是一种包含 Unicode 字符
的序列。如果 str 只含有 7 位 ASCII 字符,那么可以通过相关的操作符来同时使
用 str 与 unicode。
‰‰ 在对输入的数据进行操作之前,使用辅助函数来保证字符序列的类型与开发者
的期望相符(有的时候,开发者想操作以 UTF-8 格式来编码的 8 位值,有的时
候,则想操作 Unicode 字符)。
第 4 条:用辅助函数来取代复杂的表达式 8
‰‰ 开发者很容易过度运用Python的语法特性,从而写出那种特别复杂并且难以理
解的单行表达式。
‰‰ 请把复杂的表达式移入辅助函数之中,如果要反复使用相同的逻辑,那就更应
该这么做。
‰‰ 使用 if/else表达式,要比用or 或 and这样的Boolean 操作符写成的表达式更加
清晰
第 5 条:了解切割序列的办法 10
切割操作的基本写法是 somelist[start:end],其中start(起始索引)所指的元素涵盖
在切割后的范围内,而 end(结束索引)所指的元素则不包括在切割结果之中
如果从列表开头获取切片,那就不要在 start 那里写上 0,而是应该把它留空,这样
代码看起来会清爽一些。
如果切片一直要取到列表末尾,那就应该把 end 留空,因为即便写了,也是多余。
在指定切片起止索引时,若要从列表尾部向前算,则可使用负值来表示相关偏移
量。如果采用下面这些写法来切割列表,那么即便是刚刚接触代码的人也能立刻明白程
序的意图。由于这些写法都不会令人惊讶,所以笔者推荐大家在代码中放心地使用。
请注意,如果使用负变量作为 start索引来切割列表,那么在极个别情况下,可
能会导致奇怪的结果。例如, somelist[-n:] 这个表达式,在n大于 1时可以正常
运作 ,如当 n 为 3 时, somelist[-3:]的结果是正常的。然而,当n 为 0时,表
达式 somelist[-0:] 则成了原列表的一份拷贝。
‰‰ 不要写多余的代码:当 start索引为0,或end索引为序列长度时,应该将其省略。
‰‰ 切片操作不会计较 start 与 end索引是否越界,这使得我们很容易就能从序列的
前端或后端开始,对其进行范围固定的切片操作(如 a[:20] 或a[-20:])。
‰‰ 对 list赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替
换成新值,即便它们的长度不同也依然可以替换。
第 6 条:在单次切片操作内,不要同时指定 start、 end 和 stride 13
除了基本的切片操作(参见本书第 5条)之外,Python还提供了 somelist[start:end:stride]
形式的写法,以实现步进式切割,也就是从每 n个元素里面取1 个出来。例如,可以指
定步进值( stride),把列表中位于偶数索引处和奇数索引处的元素分成两组:
问题在于,采用 stride方式进行切片时,经常会出现不符合预期的结果。例如,
Python 中有一种常见的技巧,能够把以字节形式存储的字符串反转过来,这个技巧就是
采用 -1 做步进值。
这种技巧对字节串和 ASCII 字符有用,但是对已经编码成 UTF-8 字节串的 Unicode
字符来说,则无法奏效
我们不应该把 stride与start 和end写在一起。如果非要用
stride,那就尽量采用正值,同时省略start和 end索引。如果一定要配合start 或 end索
引来使用 stride,那么请考虑先做步进式切片,把切割结果赋给某个变量,然后在那个
变量上面做第二次切割。
上面这种先做步进切割,再做范围切割的办法 ,会多产生一份原数据的浅拷贝。
执行第一次切割操作时,应该尽量缩减切割后的列表尺寸。如果你所开发的程序对执行
时间或内存用量的要求非常严格,以致不能采用两阶段切割法,那就请考虑 Python 内
置的 itertools 模块。该模块中有个islide方法,这个方法不允许为start、end或 stride指
定负值(参见本书第 46 条)。 8
‰‰ 既有 start和end,又有stride的切割操作,可能会令人费解。
‰‰ 尽量使用 stride为正数,且不带start 或 end索引的切割操作。尽量避免用负数
做 stride。
‰‰ 在同一个切片操作内,不要同时使用start、end和 stride。如果确实需要执行这
种操作,那就考虑将其拆解为两条赋值语句,其中一条做范围切割,另一条做
步进切割,或考虑使用内置 itertools 模块中的islice。
第 7 条:用列表推导来取代 map 和 filter 15
Python 提供了一种精练的写法,可以根据一份列表来制作另外一份。这种表达式
称为 list comprehension(列表推导)。例如,要用列表中每个元素的平方值构建另一份
列表。如果采用列表推导来实现,那就同时指定制作新列表时所要迭代的输入序列,以
及计算新列表中每个元素的值时所用的表达式。
除非是调用只有一个参数的函数,否则,对于简单的情况来说,列表推导要比内置
的 map 函数更清晰。如果使用 map,那就要创建 lambda函数,以便计算新列表中各个
元素的值,这会使代码看起来有些乱
列表推导则不像 map 那么复杂,它可以直接过滤原列表中的元素,使得生成的新
列表不会包含对应的计算结果。例如,在计算平方值时,我们只想计算那些可以为 2 所
整除的数。如果采用列表推导来做,那么只需在循环后面添加条件表达式即可:
A = [1,2,3,3,42,4,5]
b = [x**3 for x in A]
A = [1,2,3,3,42,4,5]
b = [x**3 for x in A]
c = [x**3 for x in A if x%2 == 0]
print(b)
print(c)
f = {"a":1,"bb":2,"cbb":3}
g = {key: value for value, key in f.items()}
h = {len(num) for num in g.values()}
print(g)
print(h)
{1: 'a', 2: 'bb', 3: 'cbb'}
{1, 2, 3}
‰‰
列表推导要比内置的
map
和
f ilter
函数清晰,因为它无需额外编写
lambda
表第 8 条:不要使用含有两个以上表达式的列表推导 16
在列表推导中,最好不要使用两个以上的表达式。可以使用两个条件、两个循环或
一个条件搭配一个循环。如果要写的代码比这还复杂,那就应该使用普通的 if 和 for 语
句,并编写辅助函数(参见本书第 16 条)。
列表推导支持多级循环,每一级循环也支持多项条件。
超过两个表达式的列表推导是很难理解的,应该尽量避免。
第 9 条:用生成器表达式来改写数据量较大的列表推导 18
列表推导(参见本书第 7 条)的缺点是:在推导过程中,对于输入序列中的每个值
来说,可能都要创建仅含一项元素的全新列表。当输入的数据比较少时,不会出问题,
但如果输入的数据非常多,那么可能会消耗大量内存,并导致程序崩溃。
例如,要读取一份文件并返回每行的字符数。若采用列表推导来做,则需把文件每
一行的长度都保存在内存中。如果这个文件特别大,或是通过无休止的 network socket
(网络套接字)来读取,那么这种列表推导就会出问题。下面的这段列表推导代码,只适
合处理少量的输入值
为了解决此问题, Python 提供了生成器表达式(generator expression),它是对列表
推导和生成器的一种泛化(generalization)。生成器表达式在运行的时候,并不会把整个
输出序列都呈现出来,而是会估值为迭代器( iterator),这个迭代器每次可以根据生成器
表达式产生一项数据。
把实现列表推导所用的那种写法放在一对圆括号中,就构成了生成器表达式。下
面给出的生成器表达式与刚才的代码等效。二者的区别在于,对生成器表达式求值的时
候,它会立刻返回一个迭代器,而不会深入处理文件中的内容。
以刚才返回的那个迭代器为参数,逐次调用内置的 next 函数,即可使其按照生成
器表达式来输出下一个值。可以根据自己的需要,多次命令迭代器根据生成器表达式来
生成新值,而不用担心内存用量激增。
使用生成器表达式还有个好处,就是可以互相组合。下面这行代码会把刚才那个生
成器表达式所返回的迭代器用作另外一个生成器表达式的输入值。