第一章 用Pythonic的方式来思考
1. 确认自己所用的python版本
# 查看版本
python --version
# 在程序中使用sys模块查询相关的值
import sys
print(sys.version_info)
print(sys.version)
2. 遵循PEP8风格指南
《Python Enhancement Proposal #8》(8号Python增强提案),又叫PEP8,它是针对Python风格代码而编订的风格指南。
pylint工具是一款流行的python源码静态分析工具,它可以自动检查受测代码是否符合PEP8风格指南,而且还能找出程序的多种常见错误。
下面列出几条绝对应该遵守的规则:
- 空白(whitespace):空白会影响代码的含义和代码清晰程度。
- 使用4个空格(space)表示缩进,不要用tab
- 每一行字符数不超过79
- 对于多行的长表达式,除了首行之外的其余各行都应该在通常的缩进级别之上在加4个空格。
- 文件中的函数和类之间应该用两个空行隔开。
- 同一个类中,各方法之间用一个空行隔开。
- 在使用下标来获取列表元素、调用函数或给关键字参数赋值时,不要在两旁加空格。
- 为变量赋值时,赋值符号的左侧和右侧应该各自加一个空格,而且只写一个就好。
- 命名:PEP8提倡采用不同的命名风格来编写程序,以便根据这些名称看出它们的角色。
- 函数、变量和属性应该用小写字母来拼写,各单词之间用下划线相连,如lowercase_underscore。
- 受保护的实例属性,应该以单个下划线开头,如_leading_underscore。
- 私有的实例属性,应该以两个下划线开头,如__double_leading_underscore。
- 类与异常应该以每个单词首字母均大写的形式来命名,如CapitalizedWord。
- 模块级别的常量,应该全部采用大写字母来拼写,各单词之间以下划线相连,如ALL_CAPS.
- 类中的实例方法,应该把首个参数命名为self,以表示该对象自身。
- 类方法的首个参数,应该命名为cls,以表示该类自身。
- 表达式和语句:《The Zen of Python》(python之禅)说:每件事都应该有直白的做法,而且最好只有一种。
- 采用内联形式的否定词,而不要把否定词放在表达式前面。如应该写
if a is not b
而不是if not a is b
。 - 不要通过检测长度的方法(如
if len(sonelist) == 0
)来判断somelist是否为[]
或''
等空值,而是应该采用if not somelist
这种写法来判断,它会假定:空值将自动评估为False。 - 不要编写单行的
if
语句、for
循环、while
循环及except
符合语句,而是应该把这些语句分成多行来书写,以示清晰。 -
import
语句应该总是放在文件开头。 - 引入模块时应使用绝对名称,而不应该根据当前模块的路径来使用相对名称。如引入bar包中的foo模块时,应完整的写出
from bar import foo
, 而不是import foo
。 - 如果一定要以相对名称来编写import语句,那就采用明确的写法:
from . import foo
。 - 文件中的那些import语句应该按顺序划分成3个部分,分别表示标准库模块、第三方模块以及自用模块。每一部分中,各import语句应该按模块的字母顺序来排列。
3. 了解bytes、str与Unicode的区别。
4. 用辅助函数来取代复杂的表达式。
- 开发者很容易过度使用Python的语法特性,从而写出那种特别复杂并且难以理解的单行表达式。
- 如果表达式比较复杂,要考虑将其拆解成小块,并把这些逻辑移入辅助函数中,如果反复使用相同的逻辑,那就更应该这么做。这会令代码更加易读,比原来的密集写法要更好。
- 使用if/else表达式要比用or或and这样的Boolen操作符写成的表达式更加清晰。
5.了解切割序列的办法。
切片操作(slice)可以轻易访问序列中的子集。所有实现了getitem和setitem这两个特殊方法的python类均可使用切片,如list、str、bytes等。
- 不要写多余的代码,当start索引为0或end索引为序列长度时,应该将其忽略。
- 切片操作不会计较start和end索引是否越界,这使得我们很容易就能从序列的前端或后端开始,对其进行范围固定的切片操作,如
a[:20],a[-20:]
。 - 对list赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即使它们的长度不同也可以替换。如:
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
a[2:7] = [99, 22, 14]
print('After ', a)
->After ['a', 'b', 99, 22, 14, 'h']
6. 在单次切片内,不要同时指定start、end和stride。
7. 用列表推导式来取代map和filter。
- 列表推导式要比内置的map和filter函数更清晰,因为它无需额外编写lambda表达式。
- 列表推导式可以跳过输入列表中的某些元素,如果改用map来做,那就必须辅以filter方能实现。
- 字典与集也支持推导表达式。
8. 不要使用含有两个以上表达式的列表推导。
- 列表推导支持多级循环,每一级循环也支持多项条件。但最好不要使用两个以上的表达式。可以使用两个条件、两个循环或一个条件搭配一个循环。如果要写的代码比这还复杂,那就应该使用普通的if和for语句,并编写辅助函数。
- 超过两个表达式的列表推导式是很难理解的,应该尽量避免。
# 把矩阵简化成一维列表,使原来的每个单元格都成为新列表的普通元素,可以使用两个for表达式的列表推导来实现
# 这些for表达式会按照从左到右的顺序来评估。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# 另一种包含多重循环的合理用法是根据输入列表来创建有两层深度的新列表。
squared = [[x**2 for x in row] for row in matrix]
# 如果在多一层循环,那么列表推导式会很长。
my_list = [
[[1, 2, 3], [4, 5, 6],
#...
]
flat = [x for sublist1 in my_lists
for sublist2 in sublist1
for x in sublist2]
# 这时不如采用循环语句来实现相同效果。
flat = []
for sublist1 in my_list:
for sublist2 in sublist1:
flat.extend(sublist2)
# 列表页支持多个if条件,处在同一循环中的多项条件,彼此之间默认形成and表达式。
a = [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
b = [x for x in a if x > 4 if x % 2 == 0]
c = [x for x in a if x > 4 and x % 2 ==0]
# 同样条件增加,会导致代码很难懂。
9. 用生成器表达式来改写数据量较大的列表推导
- 列表推导的缺点是:在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表。如果输入的数据非常多,可能会消耗大量内存,并导致程序崩溃。
- 为了解决此问题,python提供了生成器表达式(generator expression),它是对列表推导和生成器的一种泛化(generalization)。生成器表达式在运行的时候,并不会把整个输出序列都呈现出来,而是会估值为迭代器(iterator),每个迭代器每次可以根据生成器表达式产生一项数据,避免内存过度使用。
- 生成器表达式的实现和推导式相似,只需要将列表推导的写法用圆括号括起来即可。对生成器表达式求值时会返回一个迭代器,而不会深入处理文件的内容。
- 把某个生成器表达式所返回的迭代器放在另一个生成器表达式的for子表达式中,即可将二者结合起来。
- 串在一起的生成器表达式执行速度很快。要注意,由生成器表达式返回的迭代器是由状态的,用过一轮后,就不要反复使用了
it = (len(x) for x in open('/tmp/my_file.txt'))
print(it) #<generator object <genexpr> at 0x101b81480>
#逐渐调用内置的next函数,即可生成下一个值
print(next(it)) #100
print(next(it)) #57
# 生成器可以互相组合
roots = ((x, x**0.5) for x in it)
#外围的迭代器推进时会推动内部的迭代器,就产生了连锁效应
print(next(roots)) #(15, 3.872983346207417)
10. 尽量用enumerate取代range
Python提供了内置的enumerate函数,可以把各种迭代器包装为生成器,以便稍后产生输出值。生成器每次产生一对输出值,前者表示循环下标,后者表示迭代器中获取的下一个序列元素。
- enumerate函数提供了一种精简的写法,可以在遍历迭代器时获知每个元素的索引
- 尽量用enumerate来改写那种将range与下标访问相结合的序列遍历代码
- 可以给enumerate提供第二个参数,一直盯开始计数时所使用的值
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
# 使用range方式遍历元素和索引
for i in range(len(flavor_list)):
print('%d: %s' % (i+1, flavor_list[i]))
# 使用enumerate方式来遍历元素和索引
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i+1, flavor))
# 指定enumerate函数开始计数时所用的值为1,默认为0
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i, flavor))
11. 用zip函数同时遍历两个迭代器。
- 不同的列表可能存在互相关联。如果想平行的迭代两份列表,可以使用zip函数。
- zip函数可以把两个或两个以上的迭代器封装为生成器,会从每个迭代器中获取该迭代器的下一个值,然后将这些值汇聚成元组。
- 如果提供的迭代器长度不等,zip会提前终止。
- itertools模块中的zip_longest函数可以平行遍历多个迭代器,而不用在乎他们的长度是否相等。
for name, count in zip(names, letters):
print(name)
12.不要在for和while循环后面写else块。
- Python有种特殊语法,可在for和while循环的内部语句块之后紧跟一个else块。
- 只有循环主体内没有break,循环后面的else都会执行。如果遇见break会跳出循环,而不再执行else块。这和if/else等语法不一样。
- 不要在循环块后面使用else块,因为这种写法既不直观,又易使人误解。可以使用辅助函数改进。
# if/else,try/except/else语句块中,else的意思是如果前面的不执行,则执行else块。
# 但for/else意思与人们的理解正好相反
for i in range(3):
print('loop %d' % i)
if i == 1:
break
else:
print('Else block!')
>>> loop 0
loop 1
# for循环遍历的序列是空,则立刻执行else块。
for x in []:
print('Never runs')
else:
print('For Else block!')
>>> For Else block!
# while循环的初始条件为false,后面跟着else,也会立即执行
while False:
print('Never runs')
else:
print('For Else block!')
>>> For Else block!
13. 合理利用try/except/else/finally结构中的每个代码块。
- finally块。如果既要将异常向上传播,又要在异常发生时执行清理工作,可以使用try/finally结构。这种结构的一种常见用途是确保程序能够可靠关闭文件句柄。
#open方法必须放在try外面,因为如果打开文件时异常,程序应该跳过finally块。
handle = open('/tmp/random_data.txt') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
finally:
handle.close() #always runs after try
- else块。try/except/else结构可以清晰描述哪些异常会由自己的代码处理,哪些异常会传播到上一级。通过使用else可以缩减try中的代码量,并把没有发生异常时索要执行的语句和try/except代码块隔开。
def load_json_key(data, key):
try:
result_dict = json.loads(data) # May raise ValueError
except ValueError as e:
raise KeyError from e
else:
return result_dict[key] # May raise KeyError
- 顺利运行try块后,若想使某些操作能在finally块的清理代码之前执行,则可将这些操作写入else块中。
```python
try:
# 读取文件并处理内容
except Exception as e:
# 应对可能发生的异常
else:
# 实时更新文件并把更新中可能出现的异常上报给上级代码
finally:
#清理代码