EffectivePython Advices Record

Recorded something 《EffectivePython》advices notes

不要通过检测长度的方法来判断somelist是否为空

不要使用如:if len(somelist) > 0来判断somelist是否为**[]“空值”**,而是应该采用if not somelist这种写法来判断,somelist为空值或者None表达式not somelist都将会为True
检测子元素somelist[i]也应该使用这种方法
Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a = None
>>> if not a:
... print("I'm none.")
...
I'm none.
>>> b = []
>>> if not b:
... print("I'm empty list.")
...
I'm empty list.
>>> c = {}
>>> if not c:
... print("I'm empty dictionary.")
...
I'm empty dictionary.

编写Python程序的时候一定要把编码和解码放在界面最外围来做

程序的核心部分应该使用Unicode字符类型(也就是Python3中的str,Python2中的unicode),而且不要对字符编码做任何假设。这种办法可以另程序接受多种类型的文本编码(Latin-1、Shift JIS和Big5),又可以保证输出的文本信息只采用一种编码形式(最好是UTF-8)
由于Python的字符类型有别,所以Python代码中经常会出现两种使用场景:

  • 开发者需要原始8位值,这些8位值表示以UTF-8格式或其他编码形式来编码的字符
  • 开发者需要操作没有特定编码形式的Unicode字符
    所以我们需要编写两个辅助函数,以便在这两种情况之间转换,使得转换后的输入数据能够符合开发者的预期
    在Python3中,我们需要编写接受str或bytes,并总返回str的方法:
    1
    2
    3
    4
    5
    6
    def to_str(bytes_or_str):
    if isinstance(bytes_or_str, bytes):
    value = bytes_or_str.decode('utf-8')
    else:
    value = bytes_or_str
    return value

另外还需要接受str或bytes,并总是返回bytes的方法

1
2
3
4
5
6
def to_bytes(bytes_or_str):
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value

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

在编写Python代码时,我们通常要面对很多个列表,而这些列表里的对象,可能也是相互关联的。通过列表推导,很容易就能根据某个表达式从源列表推算出一份派生类表

1
2
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]

对于本例中的派生列表和源列表来说,相同索引处的两个元素之间有着关联。如果想平行地迭代这两份列表,那么可根据names源列表的长度来执行循环

1
2
3
4
5
6
7
8
9
10
11
12
longest_name = None
max_letters = 0

for i in range(len(names)):
count = letters[i]
if count > max_letters:
longest_name = names[i]
max_letter = count

print(longest_name)
>>>
Cecilia

上面这段代码的问题在于,整个循环语句看上去很乱。用下标来访问names和letters会使代码不易阅读。用循环下标i来访问数组的写法一共出现了两次。改用enumerate来做可以稍微缓解这个问题。但是仍然不够理想

1
2
3
4
5
for i, name in enumerate(names):
count = letters[i]
if count > max_letters:
longest_name = names[i]
max_letters = count

使用内置的zip函数能够使上述代码变得更加简洁。在Python3中的zip函数,可以把两个或者两个以上的迭代器封装为生成器,以便稍后求值。这种zip生成器,会从每个迭代器中获取该迭代器的下一个值,然后把这些值汇聚成元组。与通过下标来访问多份列表的那种写法相比,这种用zip写出来的代码更加明晰

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

zip存在一个问题,如果两个迭代器的长度不一致,zip会自动提前终止

itertools内置模块中的zip_longest函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等

try/except/else

try/except/else 结构可以清晰地描述出哪些异常会由自己的代码来处理、哪些异常会传播到上一级。如果try块没有发生异常,那么就执行else块。有了这种else块,我们可以尽量缩减try块内的代码量,使其更加易读。例如,要从字符串中加载JSON字典数据,然后返回字典里某个键所对应的值

1
2
3
4
5
6
7
def load_json(data, key):
try:
result_dict = json.loads(data) # May raise ValueError
except:
raise KeyError from e
else:
return result_dict[key] # May raise KeyError

如果数据不是有效的JSON格式,那么用json.loads解码时,会产生ValueError。这个异常会由except块来捕获并处理。如果能够解码,那么else块里的查找语句就会执行,它会根据键来查出相关的值。查询时若有异常,则该异常会向上传播,因为查询语句并不在刚才那个try块的范围内。这种else子句,会把try/except后面的内容和except本身区分开,使异常的传播行为变得更加清晰

用生成器表达式来改写数据量较大的列表推导式

列表推导缺点–在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表。当输入的数据比较少时,不会出现问题,但如果输入的数据非常多,那么可能会消耗大量的内存,并导致程序崩溃
例如要读取一份文件并返回每行的字符数。若采用列表推导来做,则需要把文件每一行的长度都保存在内存中。如果这个文件特别大,或者是通过无休止的network socket来读取,那么这种列表推到就会出问题。下面这段列表推到代码,只适合处理少量的输入值

1
2
3
4
value = [len(x) for x in open('/tmp/test_file.txt')]
print(value)
>>>
[2,100,200,43,100]

为了解决此问题,Python提供了生成器表达式,它是对列表推导和生成器的一种泛化。生成器表达式在运行的时候,并不会把整个输出序列都呈现出来,而是会估值为迭代器,这个迭代器每次可以根据生成器表达式产生一项数据。把实现列表推到所用的那种写法放在一对括号中,就构成了生成器表达式。下面给出的生成器表达式与刚才的代码等效。二者的区别在于,对于生成器表达式求值的时候,它会立刻返回一个迭代器,而不会深入文件中的内容。以刚才返回的迭代器为参数,逐次调用内置的next函数,即可使其按照生成器表达式来输出下一个值。可以根据自己的需求,多次命令迭代器根据生成器表达式来生成新值,而不用担心内存用量激增

1
2
3
4
5
6
it = (len(x) for x in open('/tmp/test_file.txt'))
print(next(it))
print(next(it))
>>>
2
100

使用生成器表达式还有个好处,就是可以相互组合。下面代码会把刚那个生成器表达式所返回的迭代器用作另一个生成器表达式的输入值。外围的迭代器每次前进时,都会推动内部那个迭代器,这就产生了连锁效应,使得执行循环、评估条件表达式、对接输入和输出等逻辑都组合在一起。这种连锁生成器表达式,可以迅速在Python中执行。如果要吧多种手法组合起来,以操作大批量的输入数据,那最好用生成器表达式来实现。只是要注意:由生成器表达式所返回的那个迭代器是有状态的,用过一轮之后,就不要反复使用了

1
2
3
4
roots = ((x, x**0.5) for x in it)
print(next(roots))
>>>
(12,23)

考虑用生成器来改写直接返回列表的函数

如果函数要产生一系列结果,那么最简单的做法就是把这些结果都放在一份列表里,并将其返回给调用者。例如:我们要查出字符串中每个词的首字母在整个字符串里的位置。下面这段代码,用append方法将这些词的首字母索引添加到result列表中,并在函数结束时将其返回给调用者

1
2
3
4
5
6
7
8
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index + 1)
return result

输入一些范例值,以验证该函数能够正常运作:

1
2
3
4
>>> address = 'Four score and seven years age...'
>>> result = index_words(address)
>>> print(result[:3])
[0, 5, 11]

第一个问题是,这个代码写得有点拥挤。每次找到新的结果,都要调用append方法。但我们真正应该强调的,并不是对result.append方法的调用,而是该方法给列表中添加的那个值,也就是index + 1。另外,函数首尾还有一行代码用来创建及返回result列表。于是,在函数主体部分的约130个字符里,重要的大概只有75个

这个函数改用生成器(generator)来写会更好。生成器是使用yield表达式的函数。调用函数时,它并不是真正的运行,而是会返回生成器。每次在这个迭代器上面调用内置的next函数时,迭代器会把生成器推进到下一个yield表达式那里。生成器传给yield的每一个值都会由迭代器返回给调用者

下面的这个生成器函数,会产生和刚才那个函数相同的效果

1
2
3
4
5
6
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
1
2
3
result = list(index_words_iter(address))
>>> print(result[:3])
[0, 5, 11]

这个函数不需要包含域result列表相交互的那些代码,因而看起来比刚才那种写法清晰许多。原来那个result列表中的元素,现在都分别传给yield表达式了。调用该生成器后所返回的迭代器,可以传给内置的list函数,以将其转换为列表

使用None和文档字符串来描述具有动态默认值的参数

有时我们想采用一种非静态的类型,来做关键字参数的默认值。例如:在打印日志消息的时候,要把相关事件的记录时间也标注在这条消息中。默认情况下,消息里面所包含的时间,应该是调用log函数那一刻的时间。如果我们以为参数的默认值会在每次执行函数时得到评估,那可能就会写出下面这种代码

1
2
3
4
5
6
7
8
>>> def log(message, when=datetime.datetime.now()):
... print('%s : %s' % (when, message))

>>> log('Hi there!')
2018-08-08 09:41:37.025822 : Hi there!
>>> time.sleep(1)
>>> log('Hi again!')
2018-08-08 09:41:37.025822 : Hi again!

两条消息的时间戳是一样的,这是因为datetime.now()只执行了一次,也就是它只在函数定义的时候执行了一次。参数的默认值,会在每个模块加载进来的时候求出,而很多模块都是在程序启动的时候加载的。包含这段代码的模块一旦加载进来参数的默认值也就固定不变了,程序不会再次执行datetime.now()
True Example

1
2
3
4
5
6
7
8
>>> def log(message, when=None):
... when = datetime.datetime.now() if when is None else when
... print('%s : %s' % (when, message))
...
>>> log('Hi there!')
2018-08-08 09:48:01.383500 : Hi there!
>>> log('Hi again!')
2018-08-08 09:48:06.394290 : Hi again!

默认值为字典的错误情况实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import json
>>> def decode(data, default={}):
... try:
... return json.loads(data)
... except ValueError:
... return default
...
>>> foo = decode('bad data')
>>> foo['stuff'] = 5
>>> bar = decode('also bad')
>>> bar['meep'] = 1
>>> print('Foo:', foo)
Foo: {'stuff': 5, 'meep': 1}
>>> print('Bar:', bar)
Bar: {'stuff': 5, 'meep': 1}

True Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def decode(data, default=None):
... if default is None:
... default = {}
... try:
... return json.loads(data)
... except ValueError:
... return default
...
>>> foo = decode('bad data')
>>> foo['stuff'] = 5
>>> bar = decode('also bad')
>>> bar['meep'] = 1
>>> print('Foo:', foo)
Foo: {'stuff': 5}
>>> print('Bar:', bar)
Bar: {'meep': 1}

用只能以关键字形式指定的参数来确保代码明晰

下面定义的这个safe_division_c函数,带有两个只能以关键字形式来指定的参数。参数列表里的*号,标志着位置参数就此终结,之后那些参数,都只能以关键字形式来指定

1
def safe_division_c(number, divisor, *, ignore_overflow=False, ignore_zero_division=False):

现在,我们就不用位置参数的形式来指定关键字参数了

1
2
3
safe_division_c(1, 10**500, True, False)
>>>
TypeError: safe_division_c() takes 2 positional arguments but 4 were given

关键字参数依然可以用关键字的形式来指定,如果不指定,也依然会采用默认值

1
2
3
4
5
6
safe_division_c(1, 0, ignore_zero_division=True)  # OK

try:
safe_division_c(1, 0)
except ZeroDivisionError:
pass # Excepted

使用内置算法与数据结构

如果Python程序要处理的数量比较可观,那么代码的执行速度会受到复杂算法拖累。然而这并不能证明Python是一门执行速度很低的语言,因为这种情况很可能是算法和数据结构选择不佳导致的
幸运的是Python的标准程序库里面,内置了各种算法与数据结构,以供开发者使用。这些常见的算法与数据结构,不仅执行速度比较快,而且还可以简化编程工作。其中某些实用工具,是很难由开发者自己正确实现出来的。所以,我们应该直接使用这些Python自带的功能,而不要重新去实现它们,以节省时间和精力

双向队列

collections模块中的deque类,是一种双向队列(double-ended queue,双端队列)。从该队列的头部或者尾部插入或移除一个元素,只需要消耗常数级别的时间,这一特性使得它非常适合用来表示先进先出队列。内置的list类型,也可以像队列那样,按照一定的顺序来存放元素。从list尾部插入或者移除元素,也仅仅需要常熟级别的时间。但是,从list头部插入或者移除元素,却会耗费线性级别的时间,这与deque的常数级别时间相比,要慢得多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from collections import deque
from time import time

start = time()
fifo = deque()
# Producter
fifo.extend([i for i in range(200000)])
# Consumer
while len(fifo) > 0:
x = fifo.popleft()
end = time()
print('deque: {}'.format(end-start))

start = time()
l = []
l.extend([i for i in range(200000)])
while len(l) > 0:
x = l.pop(0)
end = time()
print('list: {}'.format(end-start))
1
2
3
# 可看到在20万级数据下一直移除第一个元素双向队列的效率要高得非常多
deque: 0.08118391036987305
list: 4.417634010314941

有序字典

标准字典是无序的。也就是说在拥有相同键值对的两个dict上面迭代,可能会出现不同的迭代顺序。标准的字典之所以会出现这种奇怪的现象,是由其快速哈希表(fast hash table)的实现方式导致的
collections模块中的OrderedDict类,是一种特殊的字典,它能按照键的插入顺序,来保存键值对在字典中的次序。在OrderedDict上面根据键来迭代,其行为是确定的。这种确定的行为,可以极大地简化测试与调试工作

1
2
3
4
5
6
7
8
9
10
11
from collections import OrderedDict

a = OrderedDict()
a['foo'] = 1
a['bar'] = 2
b = OrderedDict()
b['foo'] = 'red'
b['bar'] = 'blue'

for value1, value2 in zip(a.values(), b.values()):
print(value1, value2)

带有默认值的字典

字典可以用来保存一些统计数据。但是,由于字典里面未必有我们要查询的那个键,所以在用字典保存计数器的时候,就必须用稍微麻烦一些的方式,才能够实现这种简单的功能

1
2
3
4
5
stats = {}
key = 'my_counter'
if key not in stats:
stats[key] = 0
stats[key] += 1

我们可以用collections模块中的defaultdict类来简化上述代码。如果字典中没有待访问的键,那么它就会把某个默认值与这个键自动关联起来。于是,我们只要提供返回默认值的函数即可,字典会调用该函数为每一个默认的键指定默认值

1
2
3
4
5
6
7
from collections import defaultdict

dict = defaultdict(int)
print(dict['a'])

# out
0

堆队列

堆(heap)是一种数据结构,很适合用来实现优先级队列。heapq模块提供了heappushheappopnsmallest等一些函数,能够在标准的list类型之中创建堆结构
各种优先级的元素,都可以按任意顺序插入堆中

1
2
3
4
5
6
7
8
9
10
11
a = []
heapq.heappush(a, 5)
heapq.heappush(a, 3)
heapq.heappush(a, 7)
heapq.heappush(a, 4)
print('Before: ', a)
# 使用sort后依然能保持堆的结构,但是添加reverse后就不能保持堆结构了
# a.sort()
print('After; ', a)
# 按照优先级弹出元素的,数值越小优先级越大
print(heapq.heappop(a), heapq.heappop(a), heapq.heappop(a), heapq.heappop(a))

二分查找

在list上面使用index方法来搜索某个元素,所耗的时间会与列表的长度成线性比例。

1
2
x = list(range(10**6))
i = x.index(991234)

bisect模块中的bisect_left等函数,提供了高效的二分折半搜索算法,能够在一系列排好顺序的元素之中搜寻某个值。由bisect_left函数所返回的索引,表示待搜寻的值在序列中的插入点(将该值插在此处,能够使序列依然保持有序)

1
i= bisect_left(x, 991234)

二分搜算法的复杂度,是对数级别的。这就意味着,用bisect来搜索包含一百个元素的列表,与用index来搜索包含14个元素的列表,所耗费的时间差不多。由此可见,这种对数级别的算法,要比线性级别的算法快很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from bisect import bisect_left
from time import time

a = [i for i in range(10000)]

start = time()
bisect_left(a, 9000)
end = time()
print('bisect: {}'.format(end - start))

start = time()
a.index(9000)
end = time()
print('list: {}'.format(end - start))

# 运行结果
bisect: 1.3828277587890625e-05
list: 0.000225067138671875

通过运行结果显而易见,binsect的搜索效率远大于index的搜索效率

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×