layout | title | keywords | category | tags | |
---|---|---|---|---|---|
post
|
编写高质量的python代码 -- 01. 用pythonic 方式来思考
|
python
|
python
|
|
2017-12-28-effective-python-01-pythonic-thinking
01. 确认自己的所使用的 python 版本
>>> import sys
>>> print(sys.version_info)
sys.version_info(major=3, minor=6, micro=3, releaselevel='final', serial=0)
>>> print(sys.version)
3.6.3 (default, Oct 4 2017, 06:09:38)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)]
02. 遵循 PEP8 风格指南
下面几条一定要遵守的规则
-- 空白
# 1. 使用 space 来进进行缩进,而不是用 tab
# 2. 和语法相关的每一层缩进都用四个空格
# 3. 每行的字符数不应该超过79
# 4. 对于占据多行的长表达式来说,除了首行之外每行多应该在通常的缩进级别上再加4个空格
# 5. 代码中的函数和类之间应该用两个空行隔开
# 6. 在同一个类中,各方法之间应该用一个空行隔开
# 7. 在使用下表来获取列表原始,调用函数或给关键字参数赋值的时候,不要在两旁添加空格
-- 命名
# 8. 为变量赋值的时候,赋值符号的左侧和右侧应该格子写上一个空格,而且只写一个就好
# 9. 函数、变量及属性应该适应小写字母来拼写,个单词之间以下划线相连 eg. lowercase_underscore
# 10. 受保护的实例属性,应该使用单下划线开头 e. _leading_underscore
# 11. 私有的实例属性,应该用双下划綫开头 e. __double_leading_underscore
# 12. 类与异常应该采用大驼峰方法命名 e. CapitalizedWord
# 13. 模块级别的常量,应该全部采用大写字母来拼写,个单词之间以下划线相连 e. ALL_CAPS
# 14. 类中的实例方法(instance method),应该把收个参数命名为 self,以表示该对象自身
# 15. 类方法(class method)的收首个参数,应该命名为 cls, 以表示该类自身
-- 表达式
# 16. 采用内联形式的否定词,不要把否定词放到整个表达式的前面。
# 应该写为 if a is not b 而不是 if not a is b
# 17. 不要通过检测长度的方法(if len(somelist) == 0)来判断 somelist 是否为 []
# 或 ''等空值,应该采用 if not somelist 写法判断
# 18. 代码中引入包的顺序应分为三部分
# 标准库模块
# 第三方模块
# 自用模块, 各 import 语句应该按照模块的字母顺序来排序
03. 了解 bytes、str 和 unicode 的区别
python3中有两种表示字符序列的类型:bytes(实例包含原始的8位值)和 str(实例包含 Unicode 字符)。
python2 中两种表示字符序列的类型: str(实例包含原始的8位值) 和 unicode(包含 Unicode字符)。
由于字符类型有别,所以Python 代码中经常会出现两种常见的使用场景:
- 开发者需要原始8位值,这些8位置表示以 UTF-8格式(或其它编码形式)来编码的字符
- 开发者需要操作没有特性编码形式的 Unicode 字符
# python3 中编写两个帮助函数
def to_str(bytes_or_str):
"""接受str或bytes,并总是返回str"""
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value # Instance of str
print(repr(to_str(u'foo')))
print(repr(to_str('foo')))
def to_bytes(bytes_or_str):
"""接受str或bytes,并总是返回bytes"""
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value # Instance of bytes
print(repr(to_bytes(b'foo')))
print(repr(to_bytes('foo')))
# python2 中编写两个辅助函数
def to_unicode(unicode_or_str):
"""接受 str 或 unicode,并总返回 Unicode"""
if isinstance(unicode_or_str, str):
value = unicode_or_str.decode('utf-8')
else:
value = unicode_or_str
return value #Instance of unicode
print(repr(to_unicode(u'foo')))
print(repr(to_unicode('foo')))
def to str(unicode_or_str):
"""接受 str 或 unicode,并总返回 str"""
if isinstance(unicode_or_str, unicode):
value = unicode_or_str.encode('utf-8')
else:
value = unicode_or_str
return value #Instance of str
print(repr(to_str(u'foo')))
print(repr(to_str('foo')))
- 要点
- 在 python3中,bytes 是一种包含8位值的序列,str 是一种包含 Unicode 字符的序列。开发者不能以> 或+ 等操作符来混同操作 bytes 和 str 实例
- 在 python2中,str 是一种包含8位值的序列,Unicode是一种包含 Unicode 字符的序列。如果 str 只包含7位 ASCII 字符,那么可以通过相关的操作符来同时使用 str 与 unicode
- 在对输入的数据进行操作之前,使用辅助函数来保证字符序列的类型与开发者的期望相符
- 从文件中读取二进制数据,或向文件中写入二进制数据时,总应该以'rb'或'wb'等二进制模式来开启文件
04. 用辅助函数来取代复杂的表达式
要从 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'], 'blue': ['0'], 'green': ['']}
# 现在要依次取出 red greep opacity(不存在的参数) 的值,常规做法
red = int(my_values.get('red', [''])[0] or 0)
green = int(my_values.get('green', [''])[0] or 0)
opacity = int(my_values.get('opacity', [''])[0] or 0)
# 表达式比较长可以修改为 if/else 方式获取,最好的方式是封装成辅助函数如下
def get_first_int(values, key, default=0):
found = values.get(key, [''])
if found[0]:
found = int(found[0]
else:
found = default
return found
# 获取上面的值只需要调用辅助方法即可如
green = get_first_int(my_values, 'green')
- 要点
- 不要过度运用 python 语法特性,写出特别复杂且难以理解的单行表达式
- 把复杂的表达式移入辅助函数之中,如果要反复使用形同的逻辑,就应该这么做
- 使用 if/else 表达式,要比用 or 或 and 这样的 Boolena 操作符写成的表达式更加清楚
05. 了解切割序列的方法
切割操作的基本写法是 somelist[start:end], 其中 start(起始索引)所指的元素涵盖在切割后的范围内,而 end(结束索引)所指的元素不包括在切割结果之中。
- 要点
- 不要写多余的代码:当 start索引为0,或 end 索引为序列长度时,应该将其省略
- 切片操作不会计较 start 与 end 索引是否越界,这使得很容易就能从序列的前端或后端开始,对其进行范围固定的切片操作(a[:20] 或 a[-20:])
- 对 list 赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换
06. 在单词切片操作内,不要同时制定 start、end 和 stride
- 要点
- 即有 start 和 end,又有 stride 的切割操作,可能会令人费解
- 尽量使用 stride 为正值,且不带 start 或 end 索引的切割操作。精良避免使用负数做 stride.
- 在同一个切片操作内,不要同时使用 start, end 和 stride。如果确实需要执行这样的操作,那就考虑将其拆解为两条赋值语句,其中一条作范围切割,另一条做步进切割,或考虑使用内置 itertools 模块中的 islice
07. 用列表推导式取代 map 和 filter
# 如下例 希望获取 a 的每个数的平方
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a] # 列表推导式实现
squares = map(lambda x: x**2, a) # map 加 lambda 实现
# 如果只需要获取可以被2整除的数
even_squares = [x**2 for x in a if x % 2 == 0]
even_squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))
# 字典也支持推导列表
In [1]: chile_ranks = {'ghost':1, 'habanero': 2, 'cayenne': 3}
In [2]: {rank:name for name, rank in chile_ranks.items()}
Out[2]: {1: 'ghost', 2: 'habanero', 3: 'cayenne'}
In [3]: {len(name) for name in chile_ranks.keys()}
Out[3]: {5, 7, 8}
- 要点
- 列表推导要比内置的 map 和 filter 函数清晰,因为无需额外编写 lambda 表达式
- 列表推导式可以跳过输入列表中的某些原始,如果改用 map 来做,必须辅以 filter 方能实现
- 字典也支持推导列表
08. 不要使用含有两个以上表达式的列表推导
- 要点:
- 列表推导支持多级循环,每一级循环也支持多项条件
- 超过两个表达式的列表推导是很难理解的,应该尽量避免
09. 用生成器表达式来改写数据量较大的列表推导
列表推导的缺点:如果输入的数据非常多,那么可能会消耗大量内存,并导致程序崩溃。
假设 /tmp/myfile.txt 文件有几行数据,每行数据的长度为 100, 57, 14, 1, 4
# 少量的数据输入
value = [len(x) for x in open('/tmp/myfile.txt')]
print(value)
>>>[100, 57, 14, 1, 4]
# 如果数据量比较大可以使用生成器,把实现列表推导所用的那种写法放到一对圆括号中,就构成了生成器表达式
# 下面给出的生成器表达式与刚才的代码等效。二者的区别在于,对生成器表达式求值时,他会立刻返回一个迭代器,而不会深入处理文件中的内容
it = (len(x) for x in open('/tmp/myfile.txt'))
print(it)
>>> at 0x10918d518>
print(next(it))
print(next(it))
>>>
100
57
# 组合生成器表达式
it = (len(x) for x in open('/tmp/adobegc.log'))
roots = ((x, x**0.5) for x in it)
In [14]: next(roots)
Out[14]: (125, 11.180339887498949)
In [15]: next(roots)
Out[15]: (115, 10.723805294763608)
In [16]: next(roots)
Out[16]: (99, 9.9498743710662)
In [17]: next(roots)
Out[17]: (126, 11.224972160321824)
In [18]: next(roots)
Out[18]: (140, 11.832159566199232)
In [19]: next(roots)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
in ()
----> 1 next(roots)
StopIteration:
- 要点
- 当输入的数据量较大时,列表推导可能会因为占用太多内存而出现问题
- 由生成器表达式所返回的迭代器,可以主次产生输出值,从而避免内存用量问题
- 把某个生成器表达式所返回的迭代器,放在另一个生成器表达式的 for 子表达式中,即可将二者结合起来
- 串在一起的生成器表达式执行速度很快
10. 尽量用 enumerate 取代 range
在对列表进行迭代的时候,通常还想知道当前元素在列表中的索引。一种方式使用 range
# 使用 range
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print("{0}:{1}".format(i+1, flavor))
'''Output:
1:vanilla
2:chocolate
3:pecan
4:strawberry
'''
# 使用内置的 enumerate 函数,输出结果和上面一样
for i, flavor in enumerate(flavor_list):
print("{0}:{1}".format(i+1, flavor))
# 还可以直接指定 enumerate 函数开始计数时所用的值
for i, flavor in enumerate(flavor_list, 1):
print("{0}:{1}".format(i, flavor))
- 要点:
- enumerate 函数提供了一种精简的写法,可以在遍历迭代器时获取每个元素的索引
- 尽量用 enumerate 来改写那种将 range 与下表访问相结合的序列遍历代码
- 可以给 enumerate 提供第二个参数,以指定开始计数时所用的值(默认为0)
11. 用 zip 函数同时遍历连个迭代器
如果两个列表有相互关联关系,如果想平行的迭代两份列表,如下
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
longest_name = None
max_letters = 0
# 使用 range 最傻,不上代码,次之使用 enumerate 迭代
for i, name in enumerate(names):
count = letters[i]
if count > max_letters:
longest_name = name
max_letters = count
print("{}->{}".format(longest_name, max_letters))
#Cecilia->7
# 使用 zip 进行平行迭代
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
print("{}->{}".format(longest_name, max_letters))
- 注意内置的 zip 有两个问题:
- Python2中的 zip 并不是生成器,而是会把开发者所提供的那些迭代器,都平行的遍历一次,在此过程中,它会把那些迭代器所产生的值汇聚成元祖,并把那些元组所构成的列表完整地返回给调用者。这可能会占用大量内存导致程序崩溃。在Python2中用zip来遍历数据量非常大的迭代器,应该使用itertools内置模块中的 izip 函数
- 如果输入的迭代器长度不同,那么 zip 会表现出比较奇怪的行为。若不能确定zip所封装的列表是否等长,则可考虑改用 itertools 内置模块中的 zip_longest 函数(在python2中叫做 izip_longest)
- 要点
- 内置的 zip 函数可以平行的遍历多个迭代器
- Python3中的 zip 相当于生成器,会在遍历过程中逐次产生元组,而Python2中的zip则是直接把这些元祖完全生成好,并一次性地返回整份列表
- 如果提供的迭代器长度不等,那么zip就会自动提前终止
- itertools 内置模块中的zip_lognest 函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等
12. 不要在 for 和 while 循环后面写 else 块
Python提供了一种很多编程语言都不支持的功能,那就是可以在循环内部的语句块后面直接编写else块
for i in range(3):
print('Loop {}'.format(i))
if i == 1:
break # 在循环里面用break 语句提前跳出,会导致程序不执行else块
else:
print('Else block!')
'''
Loop 0
Loop 1
'''
for i in range(3):
print('Loop {}'.format(i))
else:
print('Else block!')
'''
Loop 0
Loop 1
Loop 2
Else block!
'''
# 初始循环条件为false的while循环,如果后面跟着else块,那它也会立即执行
while False:
print('Never runs')
else:
print('Whie Else block!')
>>> While Else block!
# 如果for循环遍历的序列为空的,会立即执行else块
for x in []:
print('Nevers runs')
else:
print('For Else block!')
>>> For Else block!
- 要点
- Python有种特殊语法,可在for及while 循环的内部语句块之后紧跟一个else块
- 只有当整个循环主体都没遇到break语句时,循环后面的else块才会执行
- 不要在循环后面使用else块,因为这种写法既不直观,又容易引人误解
13. 合理利用 try/except/else/finally 结构中的每个代码块
混合使用示例: 从文件中读取某项事物的描述信息,处理该事务,然后就地更新该文件。
UNDEFINED = object()
def divide_josn(path):
handler = open(path, 'r+') # May raise IOError
try:
data = handler.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (
op['numerator']/
op['denominator']) # May raise ZeroDivisionError
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块后,若想是某些操作能在finally块的清理代码之前执行,则可将这些操作写到else块中