如何在Python中使用生成器和产量

235 阅读9分钟

如何在Python中使用生成器和产量

今天我们将讨论Python中的生成器,它们与普通函数有什么不同,以及为什么你应该使用它们。

什么是Python中的生成器?

你是否曾经遇到过这样的情况:你需要读取大的数据集或文件,而这些数据集或文件要加载到内存中是很难的?或者你想建立一个迭代器,但是生产者函数非常简单,以至于你的大部分代码只是围绕着建立迭代器而不是产生所需的值?这些都是生成器可以真正有用和简单的一些场景。

PEP 255中引入的生成器函数是一种特殊的函数,它返回某种懒惰的迭代器。有一些对象你可以像列表一样进行循环,然而,与列表不同,懒惰迭代器不在内存中存储其内容。生成器函数与迭代器相比,其优点之一是需要编码的代码量。

介绍完后,让我们看看生成器的一些实际例子。


生成器的一些用例

读取大文件

生成器的一个常见用例是处理大文件或数据流,比如说CSV文件。比方说,我们需要计算一个文本文件有多少行,我们的代码可能看起来像这样。

csv_gen = csv_reader("some_file.txt")
row_count = 0

for row in csv_gen:
    row_count += 1

print(f"Row count is {row_count}")

与我们的csv_reader 函数以如下方式实现。

def csv_reader(file_name):
    file = open(file_name)
    result = file.read().split("\n")
    return result

这下子就非常清楚和简单了。我们的csv_reader 函数将简单地将文件打开到内存中,并读取所有的行,然后它将分割这些行,并与文件数据形成一个数组,因此我们上面的代码将完美地工作,或者说我们是这样认为的。

如果文件包含几千行,这段代码可能会在任何现代计算机中工作,然而,如果文件足够大,我们将开始有一些问题。这些问题可以从机器开始变慢,到程序杀死机器,以至于我们需要终止程序,到最终。

Traceback (most recent call last):
  File "ex1_naive.py", line 22, in <module>
    main()
  File "ex1_naive.py", line 13, in main
    csv_gen = csv_reader("file.txt")
  File "ex1_naive.py", line 6, in csv_reader
    result = file.read().split("\n")
MemoryError

我们的程序崩溃了。文件太大,无法加载到内存中,导致python引发异常并崩溃。

那么我们如何解决这个问题呢?好吧......我们知道通过生成器,我们有办法建立简单的迭代器,所以这应该有帮助,现在让我们看看用生成器建立的csv_reader 函数。

def csv_reader(file_name):
    for row in open(file_name, "r"):
        yield row

它仍然非常简单,看起来比以前更优雅,但那边的yield 关键字是什么?

yield 关键字是使这个函数成为生成器而不是普通函数的原因。与return 不同的是,yield 将通过保存函数的所有状态来暂停该函数,并在以后的连续调用中从该点继续。在这两种情况下,表达式都将返回给调用者执行。

当一个函数包含yield 时,Python 会自动 (在幕后) 实现一个迭代器,为我们应用所有需要的方法,如__iter__()__next__() ,所以我们不需要担心任何问题。

回到我们的例子,如果我们现在决定执行我们的代码,我们会得到如下的东西。

Row count is 65123455

根据你的文件会有一个不同的数字,但重要的是它能工作!我们将懒惰地加载文件。我们将懒惰地加载文件,所以我们将最小化我们的内存负载,这是一个非常简单和优雅的解决方案。

但这并不是故事的结束,还有更简单、更有趣的方法来实现生成器,那就是定义一个生成器表达式(也叫生成器理解),它的语法看起来非常像列表理解。

让我们看看那会是什么样子的

csv_gen = (row for row in open(file_name))

很漂亮,不是吗?只要记住这些主要的区别。

  • 使用yield ,会产生一个生成器对象
  • 使用return ,将只产生文件的第一行。

生成一个无限的序列

生成器的另一个常见情况是生成一个无限的序列。在 Python 中,当你使用一个有限序列时,你可以简单地调用range() 并在一个列表上下文中评估它,例如。

a = range(5)
print(list(a))
[0, 1, 2, 3, 4]

我们可以做同样的事情,使用这样的生成器生成一个无限的序列

def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1

而且你可以用它来打印数值

for i in infinite_sequence():
    print(i, end=" ")

虽然这将是一个超级快的过程,而且会 "永远 "运行下去,所以你将不得不按CTRL+C 或MAC的替代方法来手动停止它,但你会看到所有的数字在屏幕上快速打印。

还有其他方法来获取数值,例如你可以按以下方法逐一获取数值。

>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
....

关于屈服的更多信息

到目前为止,我们看了生成器的简单情况,以及yield 语句,然而,就像所有的 Python 事物一样,它并没有结束,围绕它还有更多的东西,尽管它的想法就是你到目前为止所学到的。

正如我们已经讨论过的,当我们使用yield 时,我们是在为函数保存本地状态,并将值表达式返回给调用者函数。但是我们保存本地状态是什么意思呢?嗯......这里就非常有趣了。当Pythonyield 语句被击中时,程序暂停了函数的执行,并将产生的值返回给调用者。当函数被暂停时,该函数的状态被保存,这包括像任何变量绑定、指令指针、内部堆栈和任何异常处理等数据。当生成器再次被调用时,状态被恢复,函数从它最后碰到的yield 语句开始继续,就像之前的屈服没有被调用,函数也没有被暂停。

相当不错让我们看一个例子来更好地理解这一点

>>> def multiple_yield():
...     value = "I'm here for the first time"
...     yield value
...     value = "My Second time here"
...     yield value
...
>>> multi_gen = multiple_yield()
>>> print(next(multi_gen))
I'm here for the first time
>>> print(next(multi_gen))
My Second time here
>>> print(next(multi_gen))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

我们第一次执行这个函数时,执行指针在开头,因此我们在第2行碰到了第一个yield ,因此在屏幕上打印了 "我第一次来这里 "的语句。第二次调用next() ,执行指针继续从第3行开始,在第4行打到第二个yield 语句,并返回 "我第二次来了",尽管技术上只在那一行😛过。当我们现在第三次调用next() ,我们得到一个错误。这是因为生成器,像所有的迭代器一样,可以被耗尽,如果你在这种情况发生后试图调用next() ,你会得到这个错误。


高级生成器方法

到目前为止,我们已经涵盖了生成器最常见的用法和结构,但还有一些东西需要介绍。随着时间的推移,Python 为生成器添加了一些额外的方法,我想在这里讨论以下内容。

  • .send()
  • .throw()
  • .close()

在我们讨论这些方法中的每一个细节之前,让我们创建一个样本生成器,我们将把它作为一个例子来使用。我们的生成器将生成素数,它的实现方式如下。

def isPrime(n):
    if n < 2 or n % 1 > 0:
        return False
    elif n == 2 or n == 3:
        return True
    for x in range(2, int(n**0.5) + 1):
        if n % x == 0:
            return False
    return True

def getPrimes():
    value = 0
    while True:
        if isPrime(value):
            yield value
        value += 1

如何使用.send()

.send() 允许你在任何时候设置生成器的值。比方说,你只想生成1000以上的质数,这时 ,就很方便了。让我们看一下这个例子。.send()

prime_gen = getPrimes()
print(next(prime_gen))
print(prime_gen.send(1000))
print(next(prime_gen))

当我们运行它时,我们会得到。

2
3
5

毫米......这并没有完全按照计划进行。而问题就出在我们实现的生成器函数上。为了使用发送方法,我们需要做一些修改,使它看起来像这样。

def getPrimes():
    value = 0
    while True:
        if isPrime(value):
            i = yield value
            if i is not None:
                value = i
        value += 1

现在我们再次运行

prime_gen = getPrimes()
print(next(prime_gen))
print(prime_gen.send(1000))
print(next(prime_gen))

然后我们得到。

2
1009
1013

很好!干得好!

如何使用.throw()

.throw() 正如你可能猜到的,允许你用生成器抛出异常。例如,这对于在某一数值上结束迭代是很有用的。

让我们看看它的操作。

prime_gen = getPrimes()

for x in prime_gen:
    if x > 10:
        prime_gen.throw(ValueError, "I think it was enough!")
    print(x)

然后我们得到。

2
3
5
7
Traceback (most recent call last):
  File "test.py", line 25, in <module>
    prime_gen.throw(ValueError, "I think it was enough!")
  File "test.py", line 15, in getPrimes
    i = yield value
ValueError: I think it was enough!

这样做的一个有趣的特点是,错误是在生成器内部产生的,可以从堆栈跟踪中看到。

如何使用.close()?

在前面的例子中,我们通过引发一个异常来停止迭代,然而,这并不是很优雅。一个更好的结束迭代的方法是通过使用.close()

prime_gen = getPrimes()

for x in prime_gen:
    if x > 10:
        prime_gen.close()
    print(x)

与输出。

2
3
5
7
11

在这种情况下,生成器停止了,我们离开了循环,没有引发任何异常。


总结

生成器,无论是作为生成器函数还是生成器表达式,对于优化我们的Python应用程序的性能都非常有用,尤其是在我们处理大数据集或文件的情况下。通过避免复杂的迭代器实现或通过其他方式自行处理数据,它们也会使你的代码更加清晰。