在本文中,您将学习如何使用Python生成器轻松创建迭代,它与迭代器和常规函数有何不同,以及为什么要使用它。
用Python构建迭代器有很多开销; 我们必须使用__iter__()和__next__()方法实现一个类,跟踪内部状态,在没有要返回的值时触发StopIteration等等。
这既冗长又违反直觉。生成器在这种情况下可以派上用场。
Python生成器是创建迭代器的简单方法。我们上面提到的所有开销都由Python的生成器自动处理。
简而言之,生成器是一个函数,它返回一个对象(迭代器),我们可以对其进行迭代(一次一个值)。
在Python中创建生成器非常简单。 就像使用yield语句而不是return语句定义普通函数一样容易。
如果一个函数包含至少一个yield语句(它可能包含其他yield或return语句),那么它就成为一个生成器函数。yield和return都将从函数返回一些值。
不同之处在于,当return语句完全终止一个函数时,yield语句会暂停该函数保存其所有状态,然后在后续调用时继续执行。
这是生成器函数与常规函数的不同之处。
生成器函数包含一个或多个yield语句。
调用时,它返回一个对象(迭代器),但不会立即开始执行。
像__iter__()和__next__()这样的方法会自动实现。因此,我们可以使用next()来遍历项目。
一旦函数产生了结果,函数就会暂停,控制就会转移给调用者。
局部变量及其状态在连续调用之间被记住。
最后,当函数终止时,在进一步调用时会自动引发StopIteration。
这是一个示例,用于说明上述所有要点。我们有一个my_gen()由几个yield语句命名的生成器函数。
# 一个简单的生成器函数 def my_gen(): n = 1 print('这是第一次打印') # 生成器函数包含yield语句 yield n n += 1 print('这是第二次打印') yield n n += 1 print('这是最后一次打印') yield n
解释器中的交互式运行如下所示。在Python Shell中运行这些命令以查看输出。
>>> # 它返回一个对象,但不立即开始执行. >>> a = my_gen() >>> # 我们可以使用next()遍历这些项. >>> next(a) 这是第一次打印 1 >>> # 一旦函数产生了结果,函数就会暂停,控制就会转移给调用者。 >>> # 局部变量及其状态在连续调用之间被记住。 >>> next(a) 这是第二次打印 2 >>> next(a) 这是最后一次打印 3 >>> # 最后,当函数终止时,在进一步调用时将自动引发StopIteration。 >>> next(a) Traceback (most recent call last): ... StopIteration >>> next(a) Traceback (most recent call last): ... StopIteration
在上面的示例中要注意的一件有趣的事情是,每次调用之间都会记住变量n的值。
与普通函数不同,局部变量在函数产生时不会被破坏。此外,生成器对象只能迭代一次。
要重新启动该过程,我们需要使用= my_gen()之类的东西来创建另一个生成器对象。
注意:最后要注意的一点是,我们可以直接将生成器与for循环一起使用。
这是因为,for循环接受一个迭代器,并使用next()函数对其进行迭代。当StopIteration被触发时,它会自动结束。了解如何在Python中实际实现for循环。
# 一个简单的生成器函数 def my_gen(): n = 1 print('这是第一次打印') # 生成器函数包含yield语句 yield n n += 1 print('这是第二次打印') yield n n += 1 print('这是最后一次打印') yield n # 使用for循环 for item in my_gen(): print(item)
运行该程序时,输出为:
这是第一次打印 1 这是第二次打印 2 这是最后一次打印 3
上面的示例用处不大,我们研究它只是为了了解背景中发生的事情。
通常,生成器函数是通过具有适当终止条件的循环来实现的。
让我们以反转字符串的生成器为例。
def rev_str(my_str): length = len(my_str) for i in range(length - 1,-1,-1): yield my_str[i] # For循环以反转字符串 # 输出: # o # l # l # e # h for char in rev_str("hello"): print(char)
在此示例中,我们使用range()函数使用for循环以相反的顺序获取索引。
事实证明,此生成器函数不仅适用于字符串,还适用于其他种类的可迭代对象,例如list,tuple等。
使用生成器表达式可以轻松地动态创建简单的生成器。它使建造生成器变得容易。
与lambda函数创建匿名函数相同,生成器表达式创建匿名生成器函数。
生成器表达式的语法类似于Python中的列表理解语法。但是将方括号替换为圆括号。
列表理解与生成器表达式之间的主要区别在于,虽然列表理解生成整个列表,但生成器表达式一次生成一个项目。
他们有点懒,只在需要时才生成项目。由于这个原因,生成器表达式比等价的列表理解的内存效率要高得多。
# 初始化列表 my_list = [1, 3, 6, 10] # 使用列表理解对每个项目进行平方 # 输出: [1, 9, 36, 100] [x**2 for x in my_list] # 同样的事情可以使用生成器表达式来完成 # 输出: <generator object <genexpr> at 0x0000000002EBDAF8> (x**2 for x in my_list)
上面我们可以看到生成器表达式没有立即产生所需的结果。相反,它返回了一个生成器对象,该对象带有按需生产的物品。
# 初始化list my_list = [1, 3, 6, 10] a = (x**2 for x in my_list) # 输出: 1 print(next(a)) # 输出: 9 print(next(a)) # 输出: 36 print(next(a)) # 输出: 100 print(next(a)) # 输出: StopIteration next(a)
生成器表达式可以在函数内部使用。以这种方式使用时,可以删除圆括号。
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
有几个原因使生成器成为一个有吸引力的实现。
与它们的迭代器类对应项相比,生成器可以以一种清晰而简洁的方式实现。下面是一个使用iterator类实现2的幂序列的示例。
class PowTwo: def __init__(self, max = 0): self.max = max def __iter__(self): self.n = 0 return self def __next__(self): if self.n > self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result
这代码很长。现在,使用生成器函数执行相同的操作。
def PowTwoGen(max = 0): n = 0 while n < max: yield 2 ** n n += 1
由于生成器自动跟踪细节,因此简洁明了,实现起来也更加简洁。
一个普通的返回序列的函数会在返回结果之前在内存中创建整个序列。如果序列中的项目数量很大,会影响效率。
而这种序列的生成器实现对内存友好,因此是首选的,因为它一次只能生成一项。
生成器是表示无限数据流的绝佳媒介。无限流无法存储在内存中,并且由于生成器一次只生成一项,因此它可以表示无限数据流。
下面的示例可以生成所有偶数(至少在理论上)。
def all_even(): n = 0 while True: yield n n += 2
生成器可用于流水线化一系列操作。最好用一个示例来说明。
假设我们有一个著名的快餐连锁店的日志文件。日志文件中有一个列(第4列),该列跟踪每小时售出的比萨的数量,我们希望将其求和以得出5年内售出的比萨的总数。
假设所有内容都是字符串,没有可用的数字被标记为“ N / A”。生成器的实现可以如下。
with open('sells.log') as file: pizza_col = (line[3] for line in file) per_hour = (int(x) for x in pizza_col if x != 'N/A') print("Total pizzas sold = ",sum(per_hour))
这种流水线高效且易于阅读(是的,非常酷!)。