0%

推导与生成

第27条:用列表推导取代map与filter

Python里面有一种很精简的写法,可以根据某个序列或可迭代对象派生出一份新的列表。用这种写法写成的表达式,叫作列表推导。假设我们要用列表中每个元素的平方值构建一份新的列表:

1
2
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a]

这种功能也可以使用内置函数map实现,它能够从多个列表中分别取出当前位置上的元素,并把它们当作参数传给映射函数,以求出新列表在这个位置上的元素值:

1
alt = map(lambda x: x**2, a)
如果映射关系比较简单,那么用列表推导来写还是要比用map简单一些,因为用map的时候,必须先把映射逻辑定义为lambda函数,这看上去稍微有点繁琐。

列表推导还有一个地方比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
2
even_squares_dict = {x: x**2 for x in a if x % 2 == 0}
threes_cubed_set = {x**3 for x in a if x % 3 == 0}

如果改用map与filter实现,那么还必须调用相应的构造器(constructor),这会让代码变得很长,需要分成多行才能写得下。这样看起来比较乱,不如使用推导机制的代码清晰。

1
2
alt_dict = dict(map(lambda x: (x, x**2), filter(lambda x: x % 2 == 0, a)))
alt_set = set(map(lambda x: (x, x**3), filter(lambda x: x % 3 == 0, a)))

第28条:控制推导逻辑的子表达式不要超过两个

列表推导除了最基本的用法外,列表推导还支持多层循环。例如,要把二维列表转化为普通的一维列表,那么可以在推导时,使用两条for子表达式。这些子表达式会按照从左到右的顺序解读。

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

这样写简单易懂,也是多层循环在列表推导中的合理用法。多层循环还可以用来重制那种两层深的结构。例如,要根据二维矩阵里每个元素的平方值来构建一个新的二维矩阵:

1
squared = [[x**2 for x in row] for row in matrix]

如果推导过程中还要再加一层循环,那么语句就会变得很长,必须把它分成多行来写,例如下面是把一个三维矩阵转化成普通一维列表的代码:

1
2
3
4
5
6
7
my_lists = [
[[1, 2, 3], [4, 5, 6]],
...
]
flat = [x for sublist1 in my_lists
for sublist2 in sublist1
for x in sublist2]
在这种情况下,采用列表推导来实现,其实并不会比传统的for循环节省多少代码。
1
2
3
4
flat = []
for sublist1 in my_lists:
for sublist2 in sublist1:
flat.extend(sublist2)

推导的时候,可以使用多个if条件,如果这些if条件出现在同一层循环内,那么它们之间默认是and关系,也就是必须同时成立。例如,如果要用原列表中大于4且是偶数的值来构建新列表,那么既可以连用两个if,也可以只用一个if,下面两种写法效果相同:

1
2
3
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子表达式都可以带有if条件。假如要根据原矩阵构建新的矩阵,把其中各元素之和大于等于10的那些行选出来,而且只保留其中能够被3整除的那些元素。这个逻辑用列表推导来写,并不需要太多的代码,但是这些代码理解起来会很困难:

1
2
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
filterd = [[x for x in row if x % 3 == 0] for row in matrix if sum(row) >= 10]
总之,在表示推导逻辑时,最多只应该写两个子表达式(例如两个if条件、两个for循环,或者一个if条件与一个for循环)。 只要实现的逻辑比这还复杂,那就应该采用普通的if和for语句来实现。 # 第29条:用赋值表达式消除推导中的重复代码 推导list、dict与set等结构时,经常要在多个地方用到同一个计算结果。例如,我们要给制作紧固件的公司编写程序以管理订单。顾客下单后,我们要判断当前库存能否满足这份订单,也就是说,要和产每种产品的数量有没有达到可以发货的最低限制(8个为一批,至少要一批,才能发货)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
stock = {
'nails': 125,
'screws': 35,
'wingnuts': 8,
'washers': 24,
}

order = ['screws', 'wingnuts', 'clips']

def get_batches(count, size):
return count // size

result = {}
for name in order:
count = stock.get(name, 0)
batches = get_batches(count, 8)
if batches:
result[name] = batches

print(result)

>>>
{'screws': 4, 'wingnuts': 1}
这段循环逻辑,如果改用字典推导来写,会简单一些:
1
2
3
result = {name: get_batches(stock.get(name, 0), 8) 
for name in order
if get_batches(stock.get(name, 0), 8)}
这样写虽然比刚才简短,但问题是,它把`get_batches(stock.get(name, 0), 8)`写了两遍。有个简单的办法可以解决这个问题,那就是在推导的过程中使用Python3.8新引入的 `:=`操作符进行赋值表达
1
2
result = {name: batches for name in order 
if (batches := get_batches(stock.get(name, 0), 8))}
在推导过程中,描述新值的那一部分也可以出现赋值表达式,但如果在其他部分引用了定义在那一部分的变量,那么程序可能就会在运行时出错:
1
2
3
4
5
6
result = {name: (tenth := count // 10)
for name, count in stock.items() if tenth > 0}

>>>
Traceback ...
NameError: name 'tenth' is not defined

但是,如果把赋值表达式移动到if条件里面,就可以解决这个问题:

1
2
result = {name: tenth for name, count in stock.items() 
if (tenth := count // 10) > 0}

第30条:不要让函数直接返回列表,应该让它逐个生成列表里面的值

如果函数要返回的是个包含许多结果的序列,那么最简单的办法就是把这些结果放到列表中。例如,我们要返回字符串里每个单词的首字母在字符串中所对应的下标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def index_words(text):
result = []
if text:
result.append(0)
for i, latter in enumerate(text):
if latter == ' ':
result.apppend(index + 1)

the_text = 'Four score and seven years ago...'
result = index_words(the_text)
print(result[:10])

>>>
[0, 5, 11, 15, 21, 27]

上面的index_words函数也可以改用生成器来实现。生成器由包含yield表达式的函数创建。下面就定义一个生成器函数,实现与刚才那个函数相同的效果:

1
2
3
4
5
6
def index_words_iter(the_text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
调用生成器函数并不会让其中的代码立刻得到执行,它会返回一个迭代器(iterator)。把迭代器传给Python内置的next函数,就可以将生成器函数推进到它的下一条yield表达式。生成器会把yield表达式的值通过迭代器返回给调用者。
1
2
3
4
5
6
7
it = index_words_iter(the_text)
print(next(it))
print(next(it))

>>>
0
5

如果确实要制作一份列表,那可以把生成器函数返回的迭代器传给内置的list函数:

1
result = list(index_words_iter(the_text))

index_words_iter相对于index_words来说,不必一次性把所有结果都保存到列表中,在数据的数据较多的情况下,index_words有可能因为耗尽内存而导致程序崩溃。

使用这些生成器函数时,只有一个地方需要注意,就是调用者无法重复使用函数所返回的迭代器,因为迭代器是有状态的(参见第31条)。

第31条:谨慎地迭代函数所接受的可迭代参数

如果函数接受的参数是个可迭代对象,那么我们可能会在函数中对其迭代多次。例如,我们要分析美国德克萨斯州的游客数量。原始数据保存在一份列表中,其中的每个元素表示每年有多少游客(单位是百万)。我们要统计每个城市的游客数占游客总数的百分比。

1
2
3
4
5
6
7
8
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)

return result

normalize函数中会对numbers参数进行两次迭代,一次是在sum函数的调用中,一次是在for循环中。

如果我们给nomalize函数传入参数的是一个列表,我们可以的得到正确的结果:

1
2
3
4
5
6
visits = [15, 35, 80]
percentages = normalize(visits)
print(percentages)

>>>
[11.538, 26.924, 61.538]

但是如果我们传给nomalize函数的是个迭代器,例如在数据规模较大,需要从文件中读取数据时:

1
2
3
4
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)

奇怪的是,对read_visits所返回的迭代器调用normalize函数之后,并没有得到结果:

1
2
3
4
5
6
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)

>>>
[]

出现这种状况的原因在于,迭代器只能进行一次迭代,并且迭代后不可重置。在sum函数中,已经对迭代器进行过一次迭代了,所以在for循环中由于没有数据可迭代,所以也就不会进行循环内部。

一种解决办法是让normalize函数接受另外一个函数,使它每次要使用迭代器时,都要向那个函数去索要:

1
2
3
4
5
6
7
8
9
10
def normalize_func(get_iter):
total = sum(get_iter())
result = []
for value in get_iter()
percent = 100 * value / total
result.append(percent)

return result

percentages = normalize_func(lambda: read_visits('my_numbers.txt'))

这么做虽然可行,但是每次调用normalize_func都需要传入一个函数,更好的方法是自定义一种容器类,并让其实现迭代器协议(iterator protocol)。

Python的for循环及相关的表达式,正是按照迭代器协议来遍历容器内容的。Python执行`for x in foo`这样的语句时,实际上会调用`iter(foo)`,也就是把foo传给内置的iter函数。这个函数会触发`foo.__iter__`的特殊方法,该方法必须返回一个迭代器对象(即要实现`__next__`特殊方法)。最后,Python会用迭代器对象反复调用内置的`next`函数,知道迭代完成。
1
2
3
4
5
6
7
8
class ReadVisits:
def __init__(self, data_path):
self.data_path = data_path

def __iter__(self):
with open(self.data_data_path) as f:
for line in f:
yield int(line)
我们只需要把新的容器传给最早的那个normalize函数即可,函数的代码无需修改:
1
2
3
4
5
6
visits = ReadVisits(p[11.538, 26.924, 61.538]ath)
percentage = normalize(visits)
print(percentages)

>>>
[11.538, 26.924, 61.538]
# 第32条:考虑用生成器表达式改写数据量较大的列表推导 列表推导可以根据输入序列中的每个元素创建一个包含派生元素的新列表。如果输入的数据量比较小,那么这么做没有问题,但如果数据量很大,那么程序就有可能因为内存耗尽而崩溃。例如,我们要读取一份文件并返回每行的字符数:
1
value = [len(x) for x in open('my_file.txt')]
上面的代码有可能因为文件行数太多而导致list过长。要想处理大规模的数据,可以使用生成器表达式来做,它扩展了列表推导和生成器机制。程序在对生成器表达式求值时,并不会让它把包含输出结果的那个序列立即构建出来,而是会把它当成一个迭代器,该迭代器每次可以根据表达式中的逻辑给出一个结果。 生成器表达式的写法与列表推导类似,只不过它是写在一对圆括号里,而不是方括号里:
1
2
3
it = (len(x) for x in open('my_file.txt'))
print(next(it))
print(next(it))

生成器表达式还有个强大的特性,就是可以组合起来,例如,可以用刚才那条生成器表达式所形成的it迭代器作为输入,编写一条新的生成器表达式:

1
roots = (x, x**0.5) for x in it)

第33条:通过yield from把多个生成器连起来用

生成器(yield)有很多好处,能够解决很多常见的问题。生成器的用途很广,所以许多程序都会频繁使用它们,而且是一个连一个地用。

例如,我们要编写一个图形程序,让它在屏幕上面移动图像,从而形成动画效果。假设要实现这样一段动画:图片先快速移动一段时间,然后暂停,接下来慢速移动一段时间。我们用生成器来表示图片在当前时间段内应该保持的速度:

1
2
3
def move(period, speed):
for _ in range(period):
yield speed

为了把完整的动画制作出来,我们需要调用三次move:

1
2
3
4
5
6
7
def animate():
for delta in move(4, 5.0)
yield delta
for delta in move(3, 0.0)
yield delta
for delta in move(2, 3.0)
yield delta

上面这种写法的问题在于,animate函数里有很多重复的地方。比如它反复使用for结构来操作生成器,而且每个for结构都使用相同的yield表达式。为了解决这个问题,我们可以改用yield from形式的表达式来实现。这种形式,会先从嵌套进去的小生成器里面取值,如果该生成器已经用完,那么程序的控制流程就会回到yield from所在的这个函数之中:

1
2
3
4
def animate():
yield from move(4, 5.0)
yield from move(3, 0.0)
yield from move(2, 3.0)

上面使用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)]