第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
 7- it1 = 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
 5- it = itertools.repeat('hello', 3) 
 print(list(it))
 >>>
 ['hello', 'hello', 'hello']
- cycle: 可以制作这样的一个迭代器,它会循环地输出某段内容之中的各个元素- 1 
 2
 3
 4
 5
 6- it = 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
 9- it1, 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
 12- keys = ['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
 10- it = 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
 6- it1 = 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
 6- it1 = 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
 12- it = 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
 12- it = 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
 9- single = 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
 5- it = 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
 5- it = itertools.combinations([1, 2, 3], 2) 
 print(list(it))
 >>>
 [(1, 2), (1, 3), (2, 3)]
- combinations_with_replacement: 和combination类似,但是它允许同一个元素在组合里多次出现:- 1 
 2
 3
 4- it = itertools.combinations_with_replacement([1, 2, 3], 2) 
 print(list(it))
 >>>
 [(1, 1), (1, 2), (1, 3), (2, 2) (2, 3), (3, 3)]