《Effective Python》Chapter1总结
最近在看《Effective Python》这本书,觉得很不错,对于今后的代码如何写得更pythonic。整理下吧,后面可以反复看,感觉很多建议都非常实用。
下面是一些重点摘要:
Chapter1.用Pythonic 方式来思考
第1条:确认自己所用的Python 版本
没什么可说的,还是尽快把代码迁往py3.x吧,顺便写下如何了解所使用的具体版本python:
➜ ~ python --version
Python 2.7.10
➜ ~ python3 --version
Python 3.5.0
import sys
print(sys.version_info)
print(sys.version)
- 有两种版本的Python处于活跃的状态:python2 和 python3;
- 有多种流行的Python运行时环境,例如:CPython,Jython,IronPython 以及 PyPy等。
第2条:遵循PEP8风格指南
整理下值得注意的PEP8规则吧:
空白:
- 使用space(空格)来表示缩进,而不要用tab(制表符);
- 一个缩进都用4个空格来表示;
命名:
- 函数,变量及属性应该用小写字母来拼写,各单词之间以下划线相连,例如:lowercase_underscore;
- 受保护的实例属性,应该以单个下划线开头,例如:_leading_underscore;
- 私有的实例属性,应该以两个下划线开头,例如:__double_leading_underscore;
- 类与异常,应该以每个单词首字母均大写的形式来命名。例如:CapitailizedWord;
- 模块级别的常量,应该全部采用大写字母来拼写,各单词之间以下划线相连,例如:ALL_CAPS;
- 类中的实例方法(instance method),应该把首个参数命名为self,以表示该对象自身;
- 类方法(class method)的首个参数,应该命名为cls,以表示该类自身。
表达式和语句
- import 语句应该总是放在文件开头;
- 引入模块的时候,总是应该使用绝对名称,而不应该根据当前模块的路径来使用相对名称。例如,引入bar包中的foo模块时,应该完整地写出from bar import foo。
- 如果一定要以相对名称来编写import 语句,那就采用明确的写法,from . import foo;
- 文件中的那些 import 语句应该按顺序划分成三个部分,分别表示标准库模块,第三方模块以及自用模块。在每一部分之中,各import 语句应该按模块的字母顺序来排列。
总而言之,用PyCharm吧,按照PEP8进行规范提示的。
第3条:了解bytes,str与unicode 的区别
感觉对目前我应用的场景不多,用的都是str,列下要点吧,按需深刻了解:
- 在python3中,bytes是一种包含8位值的序列,str是一种包含Unicode字符的序列。开发者不能以> 或+ 等操作符来混同操作bytes 和str 实例。
- 在python2中,str是一种包含8位值的序列,unicode是一种包含Unicode字符的序列。如果str只含有7位ASCII字符,那么可以通过相关的操作符来同时使用str 与 unicode。
第4条:用辅助函数来取代复杂的表达式,而不是一味一行式来表示所有的逻辑
举个例子:要从URL 中解码查询字符串。每个参数都可以表示一个整数:
from urllib.parse import parse_qs
my_values = parse_qs('red=5&blue=0&green=', keep_blank_values=True)
print(repr(my_values))
>>> {'red':['5'], 'green':[''], 'blue':['0']}
如果我们想要对于空值或者不存在的值统一返回默认值0,怎么处理比较好?
可以使用一行式表示,用or操作进行逻辑短路开关作用,但这样可读性很差:
red = int(my_values.get('red', [''])[0] or 0)
# or操作符的作用:如果or前半段为true的话,后面的逻辑不会再走
也可以使用三元表达式来表示,提高可读性:
red = my_values.get('red', [''])
red = int(red[0]) if red[0] else 0
所以可以总结成辅助函数了,如果频繁使用这种逻辑,就更应该抽出来封成函数:
def get_first_int(values, key, default=0):
found = values.get(key, [''])
if found[0]:
found = int(found[0])
else:
found = default
return found
red = get_first_int(my_values, 'red')
由此得到要点:
- 开发者很容易过度运用Python的语法特性,从而写出那种特别复杂并且难以理解的单行表达式。
- 请把复杂的表达式移入辅助函数之中,如果要反复使用相同的逻辑,那就更应该这么做。
- 使用 if / else 表达式,要比用 or 或 and 这样的 Boolean 操作符写成的表达式更加清晰。
第5条:了解切割序列的办法
要点:
- 不要写多余的代码:当start 索引为0,或end索引为序列长度时,应该将其省略;
- 切片操作不会计较start 与 end索引是否越界,这使得我们很容易就能从序列的前端或后端开始,对其进行范围固定的切片操作(如 a[:20] 与 a[-20:])。
- 对list 赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换。
第6条:在单次切片操作内,不要同时指定start,end 和 stride
要点:
- 既有start 和end, 又有stride 的切割操作,可能会令人费解。
- 尽量使用stride 为正数,且不带 start 或end 索引的切割操作。尽量避免用负数做stride。
- 在同一个切片操作内,不要同时使用start, end和 stride。如果确实需要执行这种操作,那就考虑将其拆解为两条赋值语句,其中一条做范围切割,另一条做步进切割,如果使用的开发程序对执行的时间或者内存用量的要求非常严格,以致不能采用两阶段切割法,那就请考虑使用内置的itertool模块中的 islice,创建的是迭代器。
第7条:用列表推导式来取代map和 filter
列表推导式与map和filter之间的比较:
列表推导式:
a = [1, 2, 3, 4, 5, 6]
even_squares = [x ** 2 for x in a if x % 2 == 0]
map 和 filter函数的组合:
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))
可见:
- 列表推导要比内置的map 和filter 函数清晰,因为它无需额外编写lambda 表达式;
- 列表推导可以跳过输入列表中的某些元素,如果改用map来做,那就必须辅以 filter 方能实现;
- 字典与集也支持推导表达式。
第8条:不要使用含有两个以上表达式的列表推导
要点:
- 列表推导支持多级循环,每一级循环也支持多项条件;
- 超过两个表达式的列表推导是很难理解的,应该尽量避免。在列表推导中,最好不要使用两个以上的表达式。可以使用两个条件,两个循环或一个条件搭配一个循环。如果要写的代码比这还复杂,那就应该使用普通的if 和for 语句,并编写辅助函数。
第9条:用生成器表达式来改写数据量较大的列表推导
列表推导的缺点是:在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表。当输入的数据比较少时,不会出问题,但如果输入的数据非常多,那么可能会消耗大量内存,并导致程序崩溃。
例如:要读取一份文件并返回每行的字符数。若采用列表推导来做,则需把文件每一行的长度都保存在内存中。如果这个文件特别大,或是通过无休止的network socket (网络套接字)来读取,那么这种列表推导就会出问题。下面的这段列表推导代码,只适合处理少量的输入值:
value = [len(x) for x in open('/tmp/my_file.txt')]
print(value)
>>>
[100, 57, 15, 1, 12, 75, 5,86, 89, 11]
为了解决此问题,Python 提供了生成器表达式,它是对列表推导和生成器的一种泛化。生成器表达式在运行的时候,并不会把整个输出序列都呈现出来,而是会估值为迭代器,这个迭代器每次可以根据生成器表达式产生一项数据。
把实现列表推导所用的那种写法放在一对圆括号中,就构成了生成器表达式。两者的区别在于,对生成器表达式求值的时候,它会立刻返回一个迭代器,而不会深入处理文件中的内容。
it = (len(x) for x in open('/tmp/my_file.txt'))
print(it)
>>>
<generator object <genexpr> at 0x101b81480>
以刚才返回的那个迭代器为参数,逐次调用内置的next函数,即可使其按照生成器表达式来输出下一个值。可以根据自己的需要,多次命令迭代器根据生成器表达式来生成新值,而不用担心内存用量激增。
print(next(it))
print(next(it))
>>>
100
57
而且对于连锁的迭代器,外围的迭代器每次前进时,都会推动内部那个迭代器,这就产生了连锁效应,使得执行循环,评估条件表达式,对接输入输出等逻辑都组合在了一起。
把某个生成器表达式所返回的迭代器,可以逐次产生输出值,从而避免了内存用量问题。
如果要把多种手法组合起来,以操作大批量的输入数据,那最好是用生成器表达式来实现。只是要注意:由生成器表达式所返回的那个迭代器是有状态的,用过一轮之后,既不要反复使用了。
第10条:尽量用 enumerate 取代 range
要点:
- enumerate 函数提供了一种精简的写法,可以在遍历迭代器时获知每个元素的索引。
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i + 1, flavor))
# 对比使用range
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print('%d: %s' % (i + 1, flavor))
- 尽量用
enumerate
来改写那种将range
与下标访问相结合的序列遍历代码。 - 可以给
enumerate
提供第二个参数,以指定开始计数时所用的值(默认为0)。
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i + 1, flavor))
第11条:用zip函数同时遍历两个迭代器
例子:存在这样的两个迭代器,怎么求出最大长度的单词及其长度?
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
下面有三种不同方法的对比:
# 使用range
longest_name = None
max_letters = 0
for i in range(len(names)):
count = letters[i]
if count > max_letters:
longest_name = names[i]
max_letters = count
# 使用enumerate
for i, name in enumerate(names):
count = letters[i]
if count > max_letters:
longest_name = name
max_letters = count
# 使用zip
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
可见,使用Python内置的zip
函数,能够令代码变得更为简洁。
要点:
- 内置的
zip
函数可以平行地遍历多个迭代器; - Python 3 中的
zip
函数相当于生成器,会在遍历过程中逐次产生元组,而Python 2 中的zip
则是直接把这些元组完全生成好,并一次性地返回整份列表; - 如果提供的迭代器长度不等,那么
zip
就会自动提前终止; itertools
内置模块中的zip_longest
函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等。
第12条:不要在for 和while 循环后面写else 块
要点:
- Python 有种特殊语法,可在
for
及while
循环的内部语句块之后紧跟一个else
块。 - 只有当整个循环主题都没遇到
break
语句时,循环后面的else
块才会执行。(要遍历的序列为空时,也会立刻执行else区块)。 - 不要在循环后面使用
else
块,因为这种写法既不直观,又容易引人误解。
用途:
如,判断某个区间的整数是否为质数:
a, b = 4, 9
for i in range(2, min(a,b) + 1):
print('Testing', i)
if a % i == 0 and b % i == 0:
print('Not coprime')
break
else:
print('Coprime')
第13条:合理利用 try/except/else/finally 结构中的每个代码块
try/except/else/finally 的示例:
UNDEFINED = object()
def divide_json(path):
handle = open(path, 'r+') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (op['numerator'] / op['denominator']) # May raise ZeroDivisiorError
except ZeroDivisionError as e:
return UNDEFINED
else:
op['result'] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result) # May raise IOError
return value
finally:
handle.close() # Always runs
其中:
- 无论
try
块是否发生异常,都可利用try/finally
复合语句中的finally
块来执行清理工作。 else
块可以用来缩减try
块中的代码量,并把没有发生异常时所要执行的语句与try/except
代码块隔开。如果try
块没有发生异常,那么就执行else
块。有了这种else
块,我们可以尽量缩减try
块内的代码量,使其更加易读。- 顺利运行
try
块后,若想使某些操作能在finally
块的清理代码之前执行,则可将这些操作写到else
块中。