推导与生成

推导(comprehension),迭代地派生出需要的数据。
生成(generation),让函数返回一批数据中的一个。生成器可以提升性能并减少内存用量,还可以让代码更容易读懂。

用推导式代替 map & filter

内容清晰、简短,不用另外定义 lambda 表达式。

a = [1, 2, 3, 4, 5, 6, 7, 8]
# map & filter
alt = map(lambda x: x ** 2, a)
alt = map(lambda x: x ** 2, filter(lambda x: x%2 == 0, a))
 
# 列表推导式
alt = [x**2 for x in range(10)]
# 字典推导式
a = {key: value for key, value in map.items() if len(key) > 10}
# 集合推导式
a = {key for key, value in map.items()}

控制列表推导式的逻辑子表达式不要超过 2 个

控制列表推导式的逻辑子表达式不要超过 2 个,如果实在比较多,应该分开完成。

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 b if x>4 and x%2==0]

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

# Before
def index_words(text):
    result = []
    if text:
	    result.append(0)
	for index, letter in enumerate(text):
		if letter == ' ':
			result.append(index + 1)
	return result
 
result = index_words("hi hi hi")
print(result)
 
# After
def index_words_iter(text):
	if text:
		yield 0
	for index, letter in enumerate(text):
		if letter == ' ':
			yield index+1
 
it = index_words_iter("hi hi hi")
print(next(it))
print(next(it))
 
result = list(index_words_iter("hi hi hi"))
print(result[:10])

谨慎地迭代函数所收到的参数

迭代器只能使用一次。如果函数的参数是迭代器类型,那么可能会在处理的时候出错。

对于下面的代码。为了避免读取文件时文件过大导致内存爆掉,所以采用了迭代器。然而,normalize 函数内部对传入的参数进行了两次迭代,一次是 sum(numbers),一次是 for 循环。由于第一次将迭代器迭代完毕,所以第二次迭代时无法获得任何数据。

def normalize(numbers):
	total = sum(numbers)
	result = []
	for value in numbers:
		percent =100*value/total
		result.append(percent)
	return result
 
def read_visits(data_path):
    with open(data_path, "r") as fp:
        for line in fp:
	        yield int(line)
 
it = read_visits("1.txt")
per = normalize(it)
print(per)  # []

下面的代码也是类似的效果:

it = read_visits("1.txt")
print(list(it))
print(list(it))

那么如何优雅的避免这种方式呢?可以新建一种容器类,让它来实现迭代器协议(iterator protocol)。不过在这之前,需要了解下 Python 迭代器协议。

Python 对一个对象进行迭代的时候,会去执行 iter(obj)。即,将 obj 传给内置的 iter 函数。iter 函数会触发 obj.__iter__ 的特殊方法,该方法会返回一个迭代器对象。最后,Python 会反复调用迭代器对象的 next 函数,直到数据耗尽。数据耗尽的标志是 next 函数抛出 StopIteration 异常。

符合迭代器协议的容器类实现方式就是在类中实现一个 __iter__ 方法,返回一个迭代器对象就可以了。

class ReadVisits:
	def __init__(self, data_path):
		self.data_path = data_path
 
	def __iter__(self):
	    with open(data_path, "r") as fp:
	        for line in fp:
		        yield int(line)

在使用的时候,直接创建容器类对象,并将其传入到函数中就可以了。而 normalize 函数内部在每次要进行迭代的时候,都会去调用 visit.__iter__,创建出新的迭代器对象,不会导致后续迭代时出现数据耗尽的情况。

visit = ReadVisits("1.txt")
per = normalize(visit)
print(per)  # [xxxxxx]

由于 normalize 函数会对传入的参数进行多次迭代,因此入参不能是普通的迭代器。为了提高代码质量,可以对 normalize 函数进行如下完善:

def normalize_defensive(numbers):
	# 方案一
	if iter(numbers) is numbers:
		raise TypeError("Must supply a container")
	# 方案二
	from collections.abs import Iterator
	if isinstance(numbers, Iterator):
		raise TypeError("Must supply a container")
 
	total = sum(numbers)
	result = []
	for value in numbers:
		percent = 100 * value / total
		result.append(percent)
	return result

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

列表推导式改写为生成器表达式:

# 列表推导式
value = [len(x) for x in open("file.txt")]
 
# 生成器表达式
it = (len(x) for x in open("file.txt"))
print(it)  # <generator object <genexpr> at xxxx>
print(next(it))
print(next(it))

生成器表达式也可以进行组合起来:

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

唯一需要注意的,仍然是生成器表达式返回的迭代器是有状态的,只能运行一轮。

通过 yield from 将多个生成器连起来用

以下面的栗子为例,animate 函数需要将多个迭代器放在一起,依次进行迭代。

def move(period, speed):
	for _ in range(period):
		yield speed
 
def pause(delay):
	for _ in range(delay):
		yield 0
 
def animate():
	for delta in move(4, 5.0):
		yield delta
	for delta in pause(3):
		yield delta
	for delta in move(2, 3.0)
		yield delta

然而,仅仅使用了三个生成器就要写三个 for 循环,会让代码显得烦琐。使用 yield from 可以改变这种现象,具体如下:

def animate_composed():
	yield from move(4, 5.0)
	yield from pause(3)
	yield from move(2, 3.0)

itertools 模块来拼装迭代器与生成器

略。