27. 用列表推导取代 map 与 filter
>>> a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> squares = [x**2 for x in a]
>>> print(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
字典和集合也有相应的推导机制。
>>> even_squares_dict = {x: x**2 for x in a if x % 2 == 0}
>>> print(even_squares_dict)
{2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
>>> threes_cubed_set = {x**3 for x in a if x % 3 == 0}
>>> print(threes_cubed_set)
{216, 729, 27}
28. 控制推导逻辑的子表达式不要超过两个
>>> 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]
>>> squared = [[x**2 for x in row] for row in matrix]
>>> print(squared)
[[1, 4, 9], [16, 25, 36], [49, 64, 81]]
如果再加一层循环,使用传统 for 循环就好:
>>> flat = []
>>> for sublist1 in my_lists:
for sublist2 in sublist1:
flat.extend(sublist2)
推导的时候可以使用多个 if 条件,如果它们出现在同一层循环内,那么它们之间默认是 and 的关系。下边两种写法效果相同:
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]
在表示推导逻辑时,最多只应该写两个子表达式(例如两个 if 条件、两个 for 循环,或者一个 if 条件与一个 for 循环)。
29. 用赋值表达式消除推导中的重复代码
stock = {
'nails': 125,
'screws': 35,
'wingnuts': 8,
'washers': 24,
}
order = ['screws', 'wingnuts', 'clips']
def get_batches(count, size):
return count // size
found = {name: batches for name in order
if (batches := get_batches(stock.get(name, 0), 8))}
print(found)
>>>
{'screws': 4, 'wingnuts': 1}
30. 不要让函数直接返回列表,应该让它逐个生成列表里的值
def index_file(handle):
offset = 0
for line in handle:
if line:
yield offset
for letter in line:
offset += 1
if letter == ' ':
yield offset
31. 谨慎地迭代函数所收到的参数
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
visits = [15, 35, 80]
percentages = normalize(visits)
为了应对规模更大的数据,现在需要让程序能够从文件中读取信息:
class ReadVisits:
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
visits = ReadVisits(path)
percentages = nomalize(visits)
这样做为什么可行呢?因为 normalize 函数里边的 sum 会触发 ReadVisits.__iter__,让系统分配一个新的迭代器对象给它。接下来,normalize 通过 for 循环计算每项数据占总值的百分比时,又会触发 __iter__,于是系统会分配另一个迭代器对象。可以在编写函数和方法时先确认一下,收到的应该是像 ReadVisits 这样的容器,而不是普通的迭代器。按照协议,如果将普通的迭代器传给内置的 iter 函数,那么函数会把迭代器本身返回给调用者,反之,如果传来的是容器类型,那么 iter 函数就会返回一个新的迭代器对象。
def normalize_defensive(numbers):
if iter(numbers) is numbers:
raise TypeError('Must supply a container')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
32. 考虑用生成器表达式改写数据量较大的列表推导
列表推导可以根据输入序列中的每个元素构建一个包含派生元素的新列表。
it = (len(x) for x in open('my_file.txt'))
生成器表达是还可以组合使用:
roots = ((x, x**0.5) for x in it)
唯一需要注意的是,生成器表达式返回的迭代器是有状态的,跑完一整轮之后,就不能继续使用了。
33. 通过 yield from 把多个生成器连起来用
def move(period, speed):
for _ in range(period):
yield speed
def pause(delay):
for _ in range(delay):
yield 0
def animate():
yield from move(4, 5.0)
yield from delay(3)
yield from move(2, 3.0)
def render(delta):
print(f'Delta: {delta:.1f}')
def run(func):
for delta in func()
render(delta)
run(animate)
>>>
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 0.0
Delta: 0.0
Delta: 0.0
Delta: 3.0
Delta: 3.0
Python 解释器看到 yield from 形式的表达式后,会想办法实现与带有普通 yield 语句的 for 循环相同的效果,而且这种实现方式要更快。
34. 不要用 send 给生成器注入数据
def wave_cascading(amplitude_it, steps):
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
amplitude = next(amplitude_it)
output = amplitude * fraction
yield output
def complex_wave_cascading(amplitude_it):
yield from wave_cascading(amplitude_it, 3)
yield from wave_cascading(amplitude_it, 4)
yield from wave_cascading(amplitude_it, 5)
def run_cascading():
amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
it = complex_wave_cascading(iter(amplitudes))
for amplitude in amplitudes:
output = next(it)
transmit(output)
run_cascading()
>>>
Output: 0.0
Output: 6.1
Output: -6.1
Output: 0.0
Output: 2.0
Output: 0.0
Output: -2.0
Output: 0.0
Output: 9.5
Output: 5.9
Output: -5.9
Output: -9.5
send 方法可以把数据注入生成器,让它成为上一条 yield 表达式的求值结果,生成器可以把这个结果赋给变量。但把 send 方法与 yield from 表达式搭配起来使用,可能导致奇怪的结果,例如会让程序在本该输出有效值的地方输出 None。通过迭代器向组合起来的生成器输入数据,要比采用 send 方法的那种方案好,所以尽量避免使用 send 方法。
35. 不要通过 throw 变换生成器的状态
def check_for_reset():
...
def announce(remaining):
print(f'{remaining} ticks remaining')
class Timer:
def __init__(self, period):
self.current = period
self.period = period
def reset(self):
self.current = self.period
def __iter__(self):
while self.current:
self.current -= 1
yield self.current
def run():
timer = Timer(4)
for current in timer:
if check_for_reset():
timer.reset()
announce(current)
run()
>>>
3 ticks remaining
2 ticks remaining
1 ticks remaining
3 ticks remaining
2 ticks remaining
3 ticks remaining
2 ticks remaining
1 ticks remaining
0 ticks remaining
throw 方法可以把异常发送到生成器刚执行过的那条 yield 表达式那里,让这个异常在生成器下次推进时重新抛出。通过 throw 方法注入异常,会让代码变得难懂,因为需要用多层嵌套的模板结构来抛出并捕获这种异常。如果确实遇到了这种特殊情况,应该通过类的 __iter__ 方法实现生成器,并且专门提供一个方法,让调用者通过这个方法来触发这种特殊的状态变换逻辑。
36. 考虑用 itertools 拼装迭代器与生成器
Python 内置的 itertools 模块里有很多函数,可以用来安排迭代器之间的交互关系。
① 连接多个迭代器
chain 可以把多个迭代器从头到尾连成一个迭代器。
it = itertools.chain([1, 2, 3], [4, 5, 6])
print(list(it))
>>>
[1, 2, 3, 4, 5, 6]
repeat 可以制作这样一个迭代器,它会不停地输出某个值,也可以通过第二个参数指定迭代器最多能输出几次。
it = itertools.repeat('hello', 3)
print(list(it))
>>>
['hello', 'hello', 'hello']
cycle 可以制作这样一个迭代器,它会循环输出某段内容之中的各项元素。
it = itertools.cycle([1, 2])
result = [next(it) for _ in range(10)]
print(result)
>>>
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
tee 可以让一个迭代器分裂成多个平行的迭代器,具体个数由第二个参数指定。如果这些迭代器的推进的速度不一致,那么程序可能要用到大量内存做缓冲,以存放进度落后的迭代器将来会用到的元素。
it1, it2, it3 = itertools.tee(['first', 'second'], 3)
print(list(it1))
print(list(it2))
print(list(it3))
>>>
['first', 'second']
['first', 'second']
['first', 'second']
zip_longest 与内置的 zip 函数类似,但如果迭代器的长度不同,那么它会用 fillvalue 参数的值来填补提前耗尽的那些迭代器所留下的空缺。
keys = ['one', 'two', 'three']
values = [1, 2]
normal = list(zip(keys, values))
print('zip: ', normal)
it = itertools.zip_longest(keys, values, fillvalue='nope')
longest = list(it)
print('zip_longest: ', longest)
>>>
zip: [('one', 1), ('two', 2)]
zip_longest: [('one', 1), ('two', 2), ('three', 'nope')]
② 过滤源迭代器中的元素
islice 可以在不拷贝数据的前提下,按照下标切割源迭代器。可以只给出切割的终点,也可以同时给出起点与终点,还可以指定步进值。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
first_five = itertools.islice(values, 5)
print('First five: ', list(first_five))
middle_odds = itertools.islice(values, 2, 8, 2)
print('Middle odds:', list(middle_odds))
>>>
First five: [1, 2, 3, 4, 5]
Middle odds: [3, 5, 7]
takewhile 会一直从源迭代器里获取元素,直到某元素让测试函数返回 False 为止。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
less_than_seven = lambda x: x < 7
it = itertools.takewhile(less_than_seven, values)
print(list(it))
>>>
[1, 2, 3, 4, 5, 6]
dropwhile 会一直跳过源序列里的元素,直到某元素让测试函数返回 True 为止,然后它会从这个地方开始逐个取值。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
less_than_seven = lambda x: x < 7
it = itertools.dropwhile(less_than_seven, values)
print(list(it))
>>>
[7, 8, 9, 10]
filterfalse 和内置的 filter 函数相反,它会逐个输出源迭代器里使得测试函数返回 False 的那些元素。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = lambda x: x % 2 == 0
filter_result = filter(evens, values)
print('Filter: ', list(filter_result))
filter_false_result = itertools.filterfalse(evens, values)
print('Filter false:', list(filter_false_result))
>>>
Filter: [2, 4, 6, 8, 10]
Filter false: [1, 3, 5, 7, 9]
③ 用源迭代器中的元素合成新元素
accumulate 会从源迭代器里取出一个元素,并把已经累计的结果与这个元素一起传给表示累加逻辑的函数,然后输出那个函数的计算结果,并把结果当成新的累计值。这与内置的 functools 模块中 reduce 函数实际上是一样的,只不过这个函数每次只给出一项累计值。如果调用者没有指定表示累加逻辑的双参数函数,那么默认的逻辑就是两值相加。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum_reduce = itertools.accumulate(values)
print('Sum: ', list(sum_reduce))
def sum_modulo_20(first, second):
output = first + second
return output % 20
modulo_reduce = itertools.accumulate(values, sum_modulo_20)
print('Modulo:', list(modulo_reduce))
>>>
Sum: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
Modulo: [1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
product 会从一个或多个源迭代器里获取元素,并计算笛卡尔积。
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, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
permutations 会考虑源迭代器所能给出的全部元素,并逐个输出由其中 N 个元素形成的每种有序排列方式,元素相同但顺序不同,算作两种排列。
it = itertools.permutations([1, 2, 3, 4], 2)
print(list(it))
>>>
[(1, 2),
(1, 3),
(1, 4),
(2, 1),
(2, 3),
(2, 4),
(3, 1),
(3, 2),
(3, 4),
(4, 1),
(4, 2),
(4, 3)]
combinations 会考虑源迭代器所能给出的全部元素,并逐个输出由其中 N 个元素形成的每种无序组合方式,元素相同但顺序不同,算作同一种组合。
it = itertools.combinations([1, 2, 3, 4], 2)
print(list(it))
>>>
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
combinations_with_replacement 与 combinations 类似,但它允许同一个元素在组合里多次出现。
it = itertools.combinations_with_replacement([1, 2, 3, 4], 2)
print(list(it))
>>>
[(1, 1),
(1, 2),
(1, 3),
(1, 4),
(2, 2),
(2, 3),
(2, 4),
(3, 3),
(3, 4),
(4, 4)]