Python中的生成器

生成器:generator

Python生成器简介

在本文中,我们将学习如何使用 Python 生成器轻松地创建迭代器、生成器与迭代器和普通函数有何不同,以及为什么应该使用生成器。

在 Python 中构建迭代器(iterator)需要做大量的工作。我们必须使用 __iter__()__next__()方法实现一个类,跟踪内部状态,并在没有返回值时引发 StopIteration异常。

这既冗长又违反直觉,在这种情况下,生成器(generator)可以拯救世界了。Python 生成器是创建迭代器的一种简单方法。上面提到的所有工作都是由生成器自动处理的,也就是说自动实现了__iter__()__next__()方法。

简单来说,生成器是一个返回对象(迭代器)的函数,我们可以对其进行迭代(每次迭代一个值)。

Python生成器的创建

用 Python 创建生成器相当简单。这就像定义一个普通函数一样简单,但是使用 yield 语句而不是 return 语句。

如果一个函数包含至少一个 yield 语句(它可能包含其他 yield 语句或 return 语句) ,它就成为一个生成器函数。yield 和 return 都会从函数返回一些值。

不同之处在于,当 return 语句完全终止一个函数时,yield 语句暂停保存其所有状态的函数,并在以后的连续调用中继续执行。

生成器 VS 函数

下面是生成器函数与普通函数的区别。

  • 生成器函数包含一个或多个 yield 语句。
  • 生成器函数在调用时,返回一个对象(迭代器) ,但不会立即开始执行。
  • 生成器函数自动实现迭代器的两个魔法方法,因此可以使用next()迭代这些项。
  • 生成器函数一旦创建,函数将暂停并将控制转移到调用方。
  • 生成器函数的局部变量及其状态在连续调用之间被记住。
  • 当生成器函数终止时,在进一步调用时自动引发 StopIteration。

这里有一个例子来说明上述所有要点。我们有一个名为 my_gen() 的生成器函数,它包含几个 yield 语句。

# 一个简单的生成器函数
def my_gen():
    n = 1
    print('第一次打印')
    # 生成器函数包含yield语句
    yield n

    n += 1
    print('第二次打印')
    yield n

    n += 1
    print('最后打印')
    yield n

运行结果:

# 返回一个生成器,但不会立即执行
a = my_gen()

# 使用next()函数进行迭代操作
next(a)
# 输出:第一次打印
# 输出:1

# 一旦生成器函数yield,函数会暂停并将控制转移到调用方

# 局部变量及其状态在连续调用之间被记住

next(a)
# 输出:第二次打印
# 输出:2

next(a)
# 输出:最后打印
# 输出:3

# 最终,函数终止。后续如果继续调用则会引发StopIteration异常。
next(a)  # 将引起 StopIteration 异常

在上面的例子中需要注意的一件有趣的事情是,变量 n 的值在每次调用之间都会被记住。

与普通函数不同,当函数 yield 时,局部变量不会被破坏。此外,生成器对象只能迭代一次。要重新启动进程,我们需要使用类似 a = my_gen()这样的代码创建另一个生成器对象。

最后要注意的是,我们可以直接使用 for 循环的生成器。

这是因为 for 循环接受一个迭代器,并使用 next() 函数对其进行迭代。当 StopIteration 被引发时,它自动结束。

如需了解在 Python 中 for 循环实际是如何实现的,请参阅“python迭代器”一文。

示例:

# 一个简单的生成器函数
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

带循环的Python生成器

上面的例子用处不大,我们研究它只是为了搞清楚背后发生了什么。通常,生成器函数是通过具有适当终止条件的回路来实现的。

让我们再来看一个生成器的例子,它用来反转字符串。

# 生成器函数
def rev_str(my_str):
    length = len(my_str)
    for i in range(length - 1, -1, -1):
        yield my_str[i]


# 用来反转字符串的for循环
for char in rev_str("hello"):
    print(char)

执行后输出:

o
l
l
e
h

在这个例子中,我们使用 range()函数给 for 循环提供逆序的索引值。

注意: 这个生成器函数不仅可以处理字符串,还可以处理列表、元组等其他类型的可迭代对象。

Python 生成器表达式

使用生成器表达式,可以很容易地动态创建简单的生成器,这使得构建生成器变得容易。

与创建匿名函数的 lambda 函数类似,生成器表达式创建匿名生成器函数。

生成器表达式的语法是:(item for item in iterable if condition)

类似于 Python 中的列表推导式,不同的是列表推导式使用方括号[ ],而生成器使用圆括号( )代替。列表推导式和生成器表达式的主要区别在于,列表推导式生成整个列表,而生成器表达式生成一个项。

并且,生成器是延迟执行(只有在被要求的时候才生产项)。由于这个原因,生成器表达式比等效的列表推导式具有更高的内存效率。

# 初始化列表
my_list = [1, 3, 6, 10]

# 使用列表推导式将每一项进行平方
list_ = [x**2 for x in my_list]

# 使用生成器可以实现同样的效果
generator = (x**2 for x in my_list)
print(list_)
print(generator)

输出:

[1, 9, 36, 100]
<generator object <genexpr> at 0x7f5d4eb4bf50>

我们可以在上面看到,生成器表达式没有立即生成所需的结果。相反,它返回一个生成器对象,该对象仅按需生成项。

以下是我们如何开始从生成器获取元素项的方法:

# 初始化列表
my_list = [1, 3, 6, 10]

a = (x**2 for x in my_list)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
next(a)

当我们运行上面的程序时,我们得到以下输出:

1
9
36
100
Traceback (most recent call last):
  File "<string>", line 15, in <module>
StopIteration

生成器表达式可以作为函数参数使用。当以这种方式使用时,圆括号可以被删除。

sum(x**2 for x in my_list)  # 输出:146
max(x**2 for x in my_list)  # 输出:100

使用 Python 生成器

有几个原因使生成器成为一个强大的实现。

  1. 易于实现

与迭代器类相比,生成器可以以简洁明了的方式实现。下面是一个使用迭代器类实现2次幂序列的示例。

这是迭代器的代码量:

class PowTwo:
    def __init__(self, max=0):
        self.n = 0
        self.max = max

    def __iter__(self):
        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

由于生成器(generator)自动跟踪细节,实现起来简洁明了。

  1. 内存效率

返回序列的普通函数将在返回结果之前,在内存中创建整个序列。如果在序列中的项目数量非常大,对内存的消耗非常大。

相反,这种序列的生成器实现则对内存友好,因为它一次只生成一个项目,所以是首选的。

  1. 表示无限流

生成器是表示无限数据流的优秀媒介。无限流不能存储在内存中,而且由于生成器一次只生成一个项,因此它们可以表示无限的数据流。

下面的生成器函数可以生成所有的偶数(至少在理论上)。

def all_even():
    n = 0
    while True:
        yield n
        n += 2
  1. 管道生成器

多个生成器可用于管道一系列的操作。这是最好的说明使用一个例子。

假设我们有一个生成器,它生成 Fibonacci 斐波拉契数列中的数字。我们还有另一个平方数的生成器。

如果我们要求出斐波拉契数列中数字的平方和,我们可以通过以下方法,将生成器函数的输出结合在一起来实现。

# 斐波拉契数列生成器
def fibonacci_numbers(nums):
    x, y = 0, 1
    for _ in range(nums):
        x, y = y, x+y
        yield x

# 平方和生成器
def square(nums):
    for num in nums:
        yield num**2

print(sum(square(fibonacci_numbers(10))))

执行后输出:

4895

这种流水线操作非常有效,并且易于阅读。

是的,简直酷到没朋友!

---END

posted @ 2022-03-24 00:16  深蓝小佛  阅读(387)  评论(0编辑  收藏  举报