《Effective Python》Chapter1总结

Date:
Categories: python
Author: sysublackbear
Tags:

最近在看《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 有种特殊语法,可在forwhile循环的内部语句块之后紧跟一个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块中。