[Effective Python笔记]一、用Pythonic方式来思考

确认自己所用的Python版本

有时候电脑里会装有多个版本的Python运行时环境,在这种情况下,使用命令行输入python命令将无法确定将会执行哪个版本。使用-- version指令来查询。

$ python --version
Python 3.6.5

遵循PEP8风格指南

《Python Enhancement Proposal #8》(8号Python增强提案)又称PEP8。它是针对Python代码格式而编订的风格指南。
它会随Python而持续更新,这里提供完整指南:https://www.python.org/dev/peps/pep-0008/
使用一致的代码风格,可以使多人工作更加便利。

了解bytes、str和unicode的区别

python3有两种字符序列的表示方法:bytes和str,前者的实例是8位值,后者的是Unicode字符。
python2也有两种,分别是str和unicode。str的实例是8位值,unicode的是Unicode字符。
在python3中bytes和str的实例可以用等价和不等价操作符,但是它们绝不会等价。

要把Unicode字符转换成二进制数据,就必须要用encode方法;反过来,则必须要用decode方法。
例如“测试编码”转换成二进制:

>>> s = "测试编码"
>>> s1 = s.encode("utf-8")
>>> print(s1)
# b'\xe6\xb5\x8b\xe8\xaf\x95\xe7\xbc\x96\xe7\xa0\x81'

Unicode:包含目前世界上所有符号的字符集,Unicode定义所有字符的长度统一为16位。可以在这个网站查找到所有字符:https://unicode-table.com/en/
utf-8:(Unicode Transformation Format),对unicode中的进行转换,以便于在存储和网络传输时可以节省空间。使用动态字节数来表示所有字符。

用辅助函数来取代复杂的表达式

举个例子,我们先取出字典中某个键对应的值,再取出值(列表)中的首个元素。
我们可能会这么写:

my_values = {'red': ['5'], 'green': [''], 'blue': ['0']}
red = my_values.get('red', [' '])[0] or 0

表达式虽然语法正确,但是却很难阅读。初次拿到代码的人,可能先要花些功夫把表达式拆解开,才能明白它的作用。
使用if/else可能会好点,但还是不够:

my_values = {'red': ['5'], 'green': [''], 'blue': ['0']}
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
}

要点

  • 使用if/else表达式,要比or或and这样的布尔操作符的表达式更清晰。

了解切割序列的方法

切片(slice)操作是由 Python 提供用于把序列切成小块的写法,切片本质上是通过浅拷贝元素组成新的容器。
序列:序列是一种泛称而非明确的类型。内置的列表(list)、元组(tuple)、字符串(str)以及range生成的列表都属于这种序列,切片操作还可以延伸到实现了__getitem__和__setitem__这两个特殊方法的Python类上。
简单的切片操作如下:

>>> seq = [1,2,3,4,5,6]

>>> seq[:]
# [1,2,3,4,5,6]
>>> seq[0:4] 
# [1,2,3,4]
>>> seq[0:-1] 
# [1,2,3,4,5]
>>> seq[2:5] = [-1, -1, -1]    
# [1, 2, -1, -1, -1]

要点

  • 在切片操作下 start 和 end 索引可以越界。而访问列表的单个元素则不行。
  • 在赋值时对左侧列表使用切割操作,会把指定范围内的对象替换为新值,而列表会根据新值的个数相应地扩张或收缩。

在单次切片操作内,不要同时指定start、end和stride

在切片操作中,我们还可以指定步进值(stride)来实现步进式切割。
常见的用法是用来把列表中位于偶数索引处和奇数索引处的元素分成两组,或是把以字节形式存储的字符串反转过来。

>>> seq = [1,2,3,4,5,6]

>>> seq[0:-1:2] 
# [1,3,5]
>>> seq2 = seq[::-1] 
# [6,5,4,3,2,1]

但是更复杂的情况呢?-2:2:-2 和 2:2:-2又是什么意思呢?
在一对中括号里写上3个数字会显得太过拥挤,从而使代码难以阅读。这种写法使得start和end的含义变得模糊不清。
为了解决这种情况,我们不应该把stride与start和end写在一起。如果实在要写,可以考虑先做步进式切片,再在那个变量上做第二次切割。

>>> seq2 = seq[::2]
# [1, 3, 5]
>>> seq2 = seq2[1:-1]
# [3]

如果对执行时间和内存用量要求非常严格,就请考虑python内置的itertools模块,该模块有个islide方法。

用列表推导来取代map和filter

我们可以用一种很精简的方式来根据一份列表推导出另一份新列表,这种方式称为列表推导(List Comprehension)。

shark_letters = [letter for letter in 'shark']    
# ['s', 'h', 'a', 'r', 'k']

shark_letters = []
for letter in 'shark':
    shark_letters.append(letter)      
 # ['s', 'h', 'a', 'r', 'k']

还可以直接过滤原列表中的元素:

shark_letters = [letter for letter in 'shark' if letter != 'a']   
 # ['s', 'h', 'r', 'k']

用内置的 filter 函数与 map 结合起来,也能达到同样的效果,但是代码会非常难懂。

shark_letters = map(lambda letter: letter, filter(lambda letter: letter != 'a', 'shark'))

(map()操作返回的是一个迭代器对象)

字典(dict)与集(set)也有和列表类似的推导式。

dic = {'a':1, 'b':2, 'c':3, 'd':4}
d1 = {key: value for key, value in dic.items()}
d2 = {value for value in dic.values()}
print(d1)
print(d2)
>>>
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
{1, 2, 3, 4}

要点

  • 列表推导式要比内置的map和filter的函数来的清晰,它不需要写lambda表达式
  • 字典和集也支持推导式

不要使用含有两个以上表达式的列表推导

列表推导支持多重循环。
例如,要把矩阵简化成一维列表。这个功能采用两个包含for表达式的列表推导式即可实现,这些for表达式会按照从左至右的顺序来评估。

matrix = [[1,2,3], [4,5,6],[7,8,9]]
flat = [x for row in matrix for x in row]
print(flat)
>>> [1, 2, 3, 4, 5, 6, 7, 8, 9]

难以理解这个推导式的话,可以这样去思考。先通过for row in matrix来迭代matrix(外循环),再通过for x in row来迭代row(内循环),最后在开头我们取x作为推导式的元素。

还有一种包含多重循环的合理用法,例如,我们要对二维矩阵中的每个单元格取平方,然后用这些平方值构建新的矩阵。

squared = [[x**2 for x in row] for row in matrix]
print(squared)
>>>
[[1,4,9], [16,25,36], [49,64,81]]

列表推导也支持多个if条件。例如,要从数字列表中选出大于4的偶数。

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]

每一级循环的for表达式后面都可以指定条件。例如,要从原矩阵中把那些本身能被3整除,且其所在行的各元素之和又大于等于10的单元格挑出来。

matrix = [[1,2,3], [4,5,6],[7,8,9]]
filtered = [[x for x in row if x % 3 == 0]
                for row in matrix if sum(row) >= 10]
print(filtered)
>>>
[[6], [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提供了生成器表达式(generator expression),它是对列表推导和生成器的一种泛化。
把实现列表推导所用的写法放在一对圆括号里,即构成了生成器表达式。两者的区别在于,对生成器表达式求值的时候,它会立刻返回一个迭代器。

it = (len(x) for x in open('/tmp/my_file.txt'))
print(it)
>>>
<generator object <genexpr> at 0x...>

通过逐次调用内置的next函数,即可使其按照生成器表达式来输出下一个值。可以根据自己的需要来生成下一个值,而不用担心内存问题。

print(next(it))
>>>
100

生成器表达式还有一个技巧,就是可以相互组合。

roots = ((x, x**2) fro x in it)

外围的迭代器每次前进时,都会推动内部的迭代器,这就产生了连锁反应。

print(next(roots))
>>>
(15, 225)

需要注意,由生成器表达式返回的迭代器是有状态的,用过一轮之后不能再次使用。
要点

  • 当输入的数据量非常大时,列表推导会占用较大的内存
  • 由生成器表达式所返回的迭代器,可以逐次产生输出值,避免了内存问题
  • 生成器表达式可以组合起来,运行速度非常快

尽量用enumerate取代range

当迭代列表时,通常还想知道当前元素在列表中的索引。例如,按照喜好程度打印出自己爱吃的冰淇淋口味。

flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i in range(len(flavor_list))
    flavor = flavor_list[i]
    print("%d:%s" % (i+1, flavor))

但是这种代码不便于理解,使用Python内置的enumerate可以把各种迭代器封装成生成器,这样的代码非常简洁。

在这里,迭代器也指各种序列以及各种支持迭代的对象。

for i, flavor in enumerate(flavor_list):
    print("%d:%s" % (i+1, flavor))

还可以指定enumerate函数开始计数时所用的值,例如从1开始计数。

for i, flavor in enumerate(flavor_list, 1):
    print("%d:%s" % (i, flavor))

要点

  • 可以给enumerate提供第二个参数,以指定开始计数时所用的值(默认为0)。

用zip函数同时遍历两个迭代器

在编写Python代码时,我们会需要面对多个列表,而这些列表中的对象可能也是相互关联的。如果想平行的迭代这两份列表,可以根据源列表的长度来执行循环。
例如

names = ['Cecilia', 'Lisa', 'Marie']
letters = [len(n) for n in names]

longest_name = None
max_letters = 0
for i, name in enumerate(names):
    count = letters[i]
    if count > max_letters:
        longest_name = name
        max_letters = count
print(longest_name)
>>>
Cecilia

使用Python内置的zip函数可以让这段代码变得简洁。
zip函数可以把两个或两个以上的迭代器封装成生成器,这种生成器会从每个迭代器中获取该迭代器的下一个值,然后把这些值汇聚成元组(tuple)。用zip写出来的代码更加明晰。

for name, count in zip(names, letters):
    if count > max_letters:
        longest_name = name
        max_letters = count

zip函数有两个问题:
第一个问题是,Python2中zip并不是生成器,而是会把开发者所提供的那些迭代器都平行的遍历一次,在此过程中,它都会把那些迭代器所产生的值汇聚成元组,并把那些元组所构成的列表完整地返回给调用者。这可能会占用大量内存并致使程序崩溃。如果要在Python2中使用zip遍历数据量非常大的迭代器,那么应该使用itertools中内置的izip函数。
第二个问题是,受封装的那些迭代器中如果长度不同,只要有一个耗尽,zip就会提前终止不再产生元组。如果待遍历的迭代器长度都相同,那么这种方式不会出问题。若不能确定zip所封装的列表是否等长,则可考虑该用itertools内置模块中的zip_longest函数。

要点

  • 内置zip函数可以平行地遍历多个迭代器
  • 如果提供的迭代器长度不等,那么zip会在最短的迭代器遍历完毕后终止。
  • itertools模块中内置的zip_longest函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等。

不要在for和while循环后面写else块

Python提供一种功能,可以在循环内部的语句块后面直接编写else块。
在for/else结构中else块的意思是:当整个循环都没有遇到break语句时,将会执行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!

在循环后面使用else块有一定意义,可以用于搜索某个事物。例如要判断两个数是否互质,若将所有可能成为公约数的值都遍历一轮,尝试完每一种可能性后,循环就结束了,于是执行完循环后,程序会紧接着执行else块。
但是这样写代码会令人相当费解,通常我们不会这样写代码,而是会用辅助函数来完成计算(例如符合条件后返回默认值)。
Python是一门简洁的语言,我们完全不应该在循环后面使用else块。
要点

  • 只有当整个循环主体都没有遇到break语句时,循环后面的else块才会执行。

合理利用try/except/else/finally结构中的每个代码块

Python程序的异常处理有四种不同的时机,这些时机可以用try、except、else和finally块来表述。

  • try:处理事务
  • except:应对try块中可能发生的相关异常
  • else:顺利运行try块后,将会在finally块的清理代码之前执行
  • finally:无论try块是否发生异常,都将运行finally块

复合语句中的每个块都有特定的用途,可以构成很多组合方式。

1.try/finally块

这种结构有一项常见用途,就是确保程序能够可靠地关闭文件句柄。

handle = open('tmp/random_data.txt')
try:
    data = handle.read()
finally:
    handle.close()

在上述代码中,read方法所抛出的异常会向上传播给调用方,而finally块中的handle.close方法则一定能执行。

2. try/except/else块

这个结构可以清晰地描述出哪些异常会由自己的代码来处理,哪些异常会传播到上一级。

def load_json_key(data, key):
    try:
        result_dict = json.load(data)
    except ValueError as e:
        raise KeyError from e
    else:
        return result_dict[key]

这种else子句,会把try/except后面的内容和except块本身区分开,使异常的传播行为变得更加清晰。

3.混合使用try/except/else/finally

如果要在复合语句中把上面几种机制都用到,那就需要完整地try/except/else/finally块。
要点

  • else块可以缩减try块的代码量,并把顺利运行后所需执行的语句与try/except块隔开
  • 顺利运行try块后,else块中的语句会在finally块之前执行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值