第27条:用列表推导取代map与filter
Python里面有一种很精简的写法,可以根据某个序列或可迭代对象派生出一份新的列表。用这种写法写成的表达式,叫作列表推导。假设我们要用列表中每个元素的平方值构建一份新的列表:
1 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
这种功能也可以使用内置函数map实现,它能够从多个列表中分别取出当前位置上的元素,并把它们当作参数传给映射函数,以求出新列表在这个位置上的元素值:
1 | alt = map(lambda x: x**2, a) |
列表推导还有一个地方比map好,就是它能够方便地过滤原列表,把某些输入值对应的计算结果从输出结果中排除。例如,假设新列表只需要纳入原列中那些偶数的平方值,那么我们可以在推导的时候再添加一个条件表达式:
1 | even_squares = [x**2 for x in a if x % 2 == 0] |
这种功能也可以通过内置的filter与map函数来实现,但是这两个函数相结合的写法要比列表推导难懂一些。
1 | alt = map(lambda x : x**2, filter(lambda x: x % 2 == 0, a)) |
上面这个写法是先用filter对a中的元素进行过滤形成新的列表,然后在对这个新的列表用map函数生成最终结果。
字典与集合也有相应的推导机制,分别叫做字典推导与集合推导,可以根据原字典与原集合创建新字典与新集合。1 | even_squares_dict = {x: x**2 for x in a if x % 2 == 0} |
如果改用map与filter实现,那么还必须调用相应的构造器(constructor),这会让代码变得很长,需要分成多行才能写得下。这样看起来比较乱,不如使用推导机制的代码清晰。
1 | alt_dict = dict(map(lambda x: (x, x**2), filter(lambda x: x % 2 == 0, a))) |
第28条:控制推导逻辑的子表达式不要超过两个
列表推导除了最基本的用法外,列表推导还支持多层循环。例如,要把二维列表转化为普通的一维列表,那么可以在推导时,使用两条for子表达式。这些子表达式会按照从左到右的顺序解读。
1 | matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
这样写简单易懂,也是多层循环在列表推导中的合理用法。多层循环还可以用来重制那种两层深的结构。例如,要根据二维矩阵里每个元素的平方值来构建一个新的二维矩阵:
1 | squared = [[x**2 for x in row] for row in matrix] |
如果推导过程中还要再加一层循环,那么语句就会变得很长,必须把它分成多行来写,例如下面是把一个三维矩阵转化成普通一维列表的代码:
1 | my_lists = [ |
1 | flat = [] |
推导的时候,可以使用多个if条件,如果这些if条件出现在同一层循环内,那么它们之间默认是and关系,也就是必须同时成立。例如,如果要用原列表中大于4且是偶数的值来构建新列表,那么既可以连用两个if,也可以只用一个if,下面两种写法效果相同:
1 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
在推导时,每一层的for子表达式都可以带有if条件。假如要根据原矩阵构建新的矩阵,把其中各元素之和大于等于10的那些行选出来,而且只保留其中能够被3整除的那些元素。这个逻辑用列表推导来写,并不需要太多的代码,但是这些代码理解起来会很困难:
1 | matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
1 | stock = { |
1 | result = {name: get_batches(stock.get(name, 0), 8) |
1 | result = {name: batches for name in order |
1 | result = {name: (tenth := count // 10) |
但是,如果把赋值表达式移动到if
条件里面,就可以解决这个问题:
1 | result = {name: tenth for name, count in stock.items() |
第30条:不要让函数直接返回列表,应该让它逐个生成列表里面的值
如果函数要返回的是个包含许多结果的序列,那么最简单的办法就是把这些结果放到列表中。例如,我们要返回字符串里每个单词的首字母在字符串中所对应的下标:
1 | def index_words(text): |
上面的index_words
函数也可以改用生成器来实现。生成器由包含yield表达式的函数创建。下面就定义一个生成器函数,实现与刚才那个函数相同的效果:
1 | def index_words_iter(the_text): |
1 | it = index_words_iter(the_text) |
如果确实要制作一份列表,那可以把生成器函数返回的迭代器传给内置的list函数:
1 | result = list(index_words_iter(the_text)) |
index_words_iter
相对于index_words
来说,不必一次性把所有结果都保存到列表中,在数据的数据较多的情况下,index_words
有可能因为耗尽内存而导致程序崩溃。
使用这些生成器函数时,只有一个地方需要注意,就是调用者无法重复使用函数所返回的迭代器,因为迭代器是有状态的(参见第31条)。
第31条:谨慎地迭代函数所接受的可迭代参数
如果函数接受的参数是个可迭代对象,那么我们可能会在函数中对其迭代多次。例如,我们要分析美国德克萨斯州的游客数量。原始数据保存在一份列表中,其中的每个元素表示每年有多少游客(单位是百万)。我们要统计每个城市的游客数占游客总数的百分比。
1 | def normalize(numbers): |
在normalize
函数中会对numbers
参数进行两次迭代,一次是在sum
函数的调用中,一次是在for
循环中。
如果我们给nomalize
函数传入参数的是一个列表,我们可以的得到正确的结果:
1 | visits = [15, 35, 80] |
但是如果我们传给nomalize
函数的是个迭代器,例如在数据规模较大,需要从文件中读取数据时:
1 | def read_visits(data_path): |
奇怪的是,对read_visits
所返回的迭代器调用normalize
函数之后,并没有得到结果:
1 | it = read_visits('my_numbers.txt') |
出现这种状况的原因在于,迭代器只能进行一次迭代,并且迭代后不可重置。在sum
函数中,已经对迭代器进行过一次迭代了,所以在for
循环中由于没有数据可迭代,所以也就不会进行循环内部。
一种解决办法是让normalize
函数接受另外一个函数,使它每次要使用迭代器时,都要向那个函数去索要:
1 | def normalize_func(get_iter): |
这么做虽然可行,但是每次调用normalize_func
都需要传入一个函数,更好的方法是自定义一种容器类,并让其实现迭代器协议(iterator protocol)。
1 | class ReadVisits: |
1 | visits = ReadVisits(p[11.538, 26.924, 61.538]ath) |
1 | value = [len(x) for x in open('my_file.txt')] |
1 | it = (len(x) for x in open('my_file.txt')) |
生成器表达式还有个强大的特性,就是可以组合起来,例如,可以用刚才那条生成器表达式所形成的it迭代器作为输入,编写一条新的生成器表达式:
1 | roots = (x, x**0.5) for x in it) |
第33条:通过yield from把多个生成器连起来用
生成器(yield)有很多好处,能够解决很多常见的问题。生成器的用途很广,所以许多程序都会频繁使用它们,而且是一个连一个地用。
例如,我们要编写一个图形程序,让它在屏幕上面移动图像,从而形成动画效果。假设要实现这样一段动画:图片先快速移动一段时间,然后暂停,接下来慢速移动一段时间。我们用生成器来表示图片在当前时间段内应该保持的速度:
1 | def move(period, speed): |
为了把完整的动画制作出来,我们需要调用三次move:
1 | def animate(): |
上面这种写法的问题在于,animate函数里有很多重复的地方。比如它反复使用for结构来操作生成器,而且每个for结构都使用相同的yield表达式。为了解决这个问题,我们可以改用yield from
形式的表达式来实现。这种形式,会先从嵌套进去的小生成器里面取值,如果该生成器已经用完,那么程序的控制流程就会回到yield from
所在的这个函数之中:
1 | def animate(): |
上面使用yield from
的代码看上去更清晰、更直观,并且这种实现方式的运行效率要更快。
第34条:不要用send给生成器注入数据
第35条:不要通过throw变换生成器的状态
说实话,第34条和第35条没怎么看懂,第一主要是生成器的这两个高级特性使用的场景也并不多,第二是感觉作者的代码示例也不太贴合实际场景中会写的代码。第36条:考虑用itertools处理迭代器与生成器
Python内置的itertools模块中有很多函数,可以用来对迭代器进行一些高级处理。下面分三大类,列出其中最重要的函数。
连接多个迭代器
chain
: 可以把多个迭代器从头连接到尾形成一个新的迭代器1
2
3
4
5
6
7it1 = iter([1, 2, 3])
it2 = iter([4, 5, 6])
it3 = itertools.chain(it1, it2)
print(list(it))
>>>
[1, 2, 3, 4, 5, 6]repeat
: 可以制作这样的一个迭代器,它会不停得输出某个值,或者通过第二个参数来控制最多能输出几次1
2
3
4
5it = itertools.repeat('hello', 3)
print(list(it))
>>>
['hello', 'hello', 'hello']cycle
: 可以制作这样的一个迭代器,它会循环地输出某段内容之中的各个元素1
2
3
4
5
6it = itertools.cycle([1, 2])
result = [next(it) for _ in range(5)]
print(result)
>>>
[1, 2, 1, 2, 1]tee
: 可以让一个迭代器分裂成多个平行迭代器,具体个数由第二个参数指定。如果这些迭代器推进的速度不一样,那么程序可能要用大量内存做缓存,以存放进度落后的迭代器会用到的元素。1
2
3
4
5
6
7
8
9it1, it2, it3 = itertools.tee([1, 2, 3], 3)
print(list(it1))
print(list(it2))
print(list(it3))
>>>
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]zip_longest
: 它与内置的zip函数类似(参见第8条),但区别是,如果源迭代器的长度不同,那么它会用fillvalue
参数的值来填补提前耗尽的那些迭代器所留下的空缺。1
2
3
4
5
6
7
8
9
10
11
12keys = ['one', 'two', 'three']
values = [1, 2]
normal = zip(keys, values)
print('zip:', list(normal))
it = itertools.zip_longest(key, values, fillvalue='nope')
print('zip_longest:', list(it))
>>>
zip: [('one', 1), ('two', 2)]
zip_longest: [('one', 1), ('two', 2), ('three', 'nope')]
过滤迭代器中的元素
islice
: 可以在不拷贝数据的前提下,按照下标切割源迭代器,这种切割方式与标准的序列切片以及步进机制类似1
2
3
4
5
6
7
8
9
10it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
first_five = itertools.islice(it, 5)
print('First Five:', list(first_five))
middle_odds = itertools.islice(it, 2, 8, 2)
print('Middle odds:', list(middle_odds))
>>>
First five: [1, 2, 3, 4, 5]
Middle odds: [3, 5, 7]takewhile
: 会一值从源迭代器里获取元素,直到某元素让测试函数返回False为止:1
2
3
4
5
6it1 = iter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
it2 = itertools.takewhile(lambda x: x < 7, it1)
print(list(it2))
>>>
[1, 2, 3, 4, 5, 6]dropwhile
: 与takewhile相反,dropwhile会一直跳过源序列里的元素,直到某元素让测试函数返回True为止,然后它会从这个地方开始逐个取值1
2
3
4
5
6it1 = iter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
it2 = itertools.dropwhile(lambda x: x < 7, it1)
print(list(it2))
>>>
[7, 8, 9, 10]filterfalse
: 和内置的filter函数相反,它会逐个输出源迭代器里使得测试函数返回False的那些元素1
2
3
4
5
6
7
8
9
10
11
12it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
evens = lambda x : x % 2 == 0
filter_result = filter(evens, it)
print('Filter:', list(filter_result))
filter_false_result = itertools.filterfalse(evens, it)
print('Filter false:', list(filter_false_result))
>>>
Filter: [2, 4, 6, 8, 10]
Filter false: [1, 3, 5, 7, 9]
用源迭代器中的元素合成新元素
accumulate
: accumulate 会从源代码迭代器取出一个元素,并把已经累计的结果与这个元素一起传给表示累加逻辑的函数,然后输出那个函数的计算结果,并把结果当成新的累计值。这与内置的functools模块中的reduce函数,实际上是一样的,只不过这个函数每次只给出一项累加值。如果调用者没有指定表示累加逻辑的函数,那么默认的逻辑就是两值相加。1
2
3
4
5
6
7
8
9
10
11
12it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
sum_reduce = itertools.accumulate(it)
print('Sum:', list(sum_reduct))
def sum_modulo_20(first, second):
output = first + second
return output % 20
modulo_reduce = itertools.accumulate(it, sum_modulo_20)
print('Modulo:' list(module_reduce))
>>>
Sum: [1, 3, 6, 10, ]product
: 会从一个或多个源迭代器里获取元素,并计算笛卡尔积,1
2
3
4
5
6
7
8
9single = itertools.product([1, 2], repeat=2)
print('Single:', list(single))
multiple = itertools.product([1, 2], ['a', 'b'])
print('Multiple:', list(multiple))
>>>
Single: [(1, 1), (1, 2), (2, 1), (2, 2)]
Multiple: [(1, '1'), (1, 'b'), (2, 'a'), (2, 'b')]product
: 会从源迭代器中能给出的全部元素,并逐个输出由其中N个元素组成的有序排列1
2
3
4
5it = itertools.permutations([1, 2, 3], 2)
print(list(it))
>>>
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]combinations
: 会从源迭代器中能给出的全部元素,并逐个输出由其中N个元素组成的无序组合1
2
3
4
5it = itertools.combinations([1, 2, 3], 2)
print(list(it))
>>>
[(1, 2), (1, 3), (2, 3)]combinations_with_replacement
: 和combination类似,但是它允许同一个元素在组合里多次出现:1
2
3
4it = itertools.combinations_with_replacement([1, 2, 3], 2)
print(list(it))
>>>
[(1, 1), (1, 2), (1, 3), (2, 2) (2, 3), (3, 3)]