基本概念
在上一节中,我们讲解了迭代的概念,同时了解了迭代器的使用。由此,我们可以引出 Python 中一个特别的用法:生成器。
顾名思义,生成器,就是可以不断动态生成数据的方法。
生成器的基础,就是我们在上节中讲解的迭代器。
还记得迭代器的“延迟求值”特性吗?我们不需要一次性加载所有数据到内存里,也能遍历潜在无限的数据序列。而生成器就应用了这一特性,从而可以在迭代过程中不断生成值。
生成器的工作原理
在使用生成器之前,我们先来了解以下生成器的工作原理。
- 生成器函数:生成器是使用一种特殊类型的函数——生成器函数创建的。这些函数使用
yield
关键字而不是return
。当调用生成器函数时,它不会立即执行函数体。相反,它会返回一个生成器对象。 - 延迟计算:生成器的关键特性是延迟计算。当我们迭代生成器时,它逐步执行生成器函数,并在遇到
yield
语句时暂停执行。也就是说,生成器不会立即计算或存储所有的值;相反,它会根据外部请求来即时生成它们。 - 状态保留:生成器会在迭代之间记住它们的状态。当我们继续迭代生成器时,它会从上次停止的地方继续,而不是从头开始。
下面让我们来看一个生成器的例子。
def my_range(n):
i = 1
while i <= n:
yield i
i += 1
x = my_range(10)
print(x) # 输出:<generator object my_range at 0x10bf717b8>
在此例中,我们创建了一个生成器,生成从 1 到 10 中的所有数字。然后,我们可以使用 for 循环或 next() 函数来迭代生成器:
# 使用 for 循环迭代生成器
for i in my_range(10):
print(i) # 输出:1 2 3 4 5 6 7 8 9 10
# 使用 next() 函数迭代生成器
print(next(x)) # 输出:1
print(next(x)) # 输出:2
print(next(x)) # 输出:3
看到了吗?在我们运行上述代码时,生成器并不会一次性计算出所有的数值;相反,它只会在每次迭代时,才会计算。
这样做有两个好处:
- 生成器的内存利用率非常高,因为它动态生成值,不会将整个序列存储在内存中。这特别适用于处理大型或潜在无限的数据序列;
- 生成器可以提高代码的性能,因为它避免了预先计算和存储值的开销。这对于处理日志文件、读取大型数据集或生成数字序列等任务非常有帮助。
好了,下面就让我们来详细探究一下生成器在 Python 中的应用吧。
生成器在 Python 中的应用
在 Python 中,创建一个生成器与创建一个普通函数很类似,最关键的区别在于:我们必须使用yield
关键字返回数值,而不是使用return
关键字。
yield
关键字在函数内部,就表明该函数是一个生成器,这样,该函数每次就只产生一个值,同时保留其在调用之间的状态。
让我们再回顾一下上面的例子。
def my_range(n):
i = 1
while i <= n:
yield i
i += 1
x = my_range(10)
# 使用 for 循环迭代生成器
for i in x:
print(i) # 输出:1 2 3 4 5 6 7 8 9 10
我们在这个例子中创建了一个生成器my_range()
,可以生成从 1 到 10 中的所有数字。然后,我们使用 for 循环来迭代该生成器。在迭代生成器my_range()
时,它一次只会产生一个值,同时保留函数在调用之间的状态。
所以,当我们在一个函数内部使用 yield
关键字时,该函数变成一个生成器函数。生成器函数在调用时不会执行整个函数体;相反,它会返回一个生成器对象,用于迭代生成器函数生成的值。
当在生成器函数在执行中遇到 yield
语句时,函数的执行会被临时暂停;而在此时,yield
语句生成的值会被返回给调用代码。函数的局部状态被保留,包括局部变量的值。
当迭代生成器时(不管是在for
循环中,还是通过调用 next()
方法),函数会从其上次暂停的位置恢复执行,即在 yield
语句之后的地方继续执行,直到遇到下一个 yield
语句或者达到函数的结尾。如果到达函数的结尾,生成器会触发一个 StopIteration
异常,表示没有更多的值可生成。
让我们再看一个例子:使用生成器来创建斐波那契数列:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
x = fibonacci()
print(next(x)) # 输出:0
print(next(x)) # 输出:1
print(next(x)) # 输出:1
print(next(x)) # 输出:2
print(next(x)) # 输出:3
print(next(x)) # 输出:5
在此例中,fibonacci
生成器不会一次性创建出所有的斐波那契数然后存储在内存中,而只是遵循“延迟求值”的方式,动态生成斐波那契数。
除了使用yield
语句创建生成器函数外,我们还可以列表推导的方式生成生成器表达式。
生成器表达式是使用括号括起来的列表推导式,它会立即生成生成器。举个例子,我们可以使用以下代码创建一个生成器,生成 1 到 10 中的所有数字的平方:
x = (x**2 for x in range(1, 11))
print(x) # 输出:<generator object <genexpr> at 0x10bf717b8>
在这个例子中,我们采用列表推导式的语法创建了一个生成器,用以生成 1 到 10 中的所有数字的平方。
虽然语法有所不同,但在使用上,生成器表达式与生成器函数没有区别。
关键字yield
和return
的区别
最后,我们再来比较以下关键字yield
和关键字return
。
如前所述,在 Python 中,关键字yield
和return
都可以用来结束函数的执行并返回一个值;但是,它们却有一些明显的区别。
- 返回值的类型不同:
return
语句返回一个普通的值,而yield
语句返回一个生成器。 - 执行方式不同:
return
语句直接结束函数的执行并返回一个值,而yield
语句生成一个值并保留函数的状态,下次调用函数时从上次停止的地方继续执行。 - 应用场景不同:
return
语句适用于大多数情况,而yield
语句只适用于创建生成器的场景。
总结
综上所述,生成器是 Python 中一项强大的工具,特别适用于处理大型数据集或需要逐个生成值的情况。
生成器通过使用yield
关键字在函数内部来定义生成器函数,使函数能够在每次迭代时产生一个值并保留其状态,从而让生成器能够高效地处理大量数据,因为它们不会一次性加载全部数据到内存中,而是按需生成。
当我们在处理日志文件、读取大型数据集、或生成无限序列等任务时,往往都会用到生成器。