推导与生成
推导(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 模块来拼装迭代器与生成器
略。