如何在Python中使用Generator和yield

  |   0 评论   |   0 浏览   |   给我丶鼓励

今天,我们将讨论 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 关键字是什么使这个函数发生器,而不是一个正常功能。与 a 的不同之处在于 returnyield 它将通过保存其所有状态来暂停该函数,并在以后的连续调用中从该点继续。在这两种情况下,表达式都将返回给调用者执行。

当一个函数包含时 yield,Python 会自动(在幕后)实现一个迭代器,该迭代器将应用所有必需的方法,例如 __iter__() and __next__() 对我们来说,因此我们不必担心其中的任何一个。

回到我们的示例,如果我们现在决定执行我们的代码,我们将得到如下信息:

Row count is 65123455

根据您的文件将是一个不同的数字,但重要的是它可以工作!我们将懒惰地加载文件,因此我们将最大程度地减少内存负载,这是一个非常简单的优雅解决方案。

但这还不是故事的结局,通过定义一个生成器表达式(也称为 generator comprehension),有更简单,更有趣的方式来实现生成器,该表达式的语法非常类似于列表推导。

让我们来看看

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 的思想都是您到目前为止学到的,但是与所有 Python 事物一样,它还没有结束。

正如我们已经讨论的那样,当使用时,我们 yield 将为函数保存本地状态,并将值表达式返回给调用者函数。但是,拯救本地国家意味着什么?好吧...这是非常有趣的地方。yield 命中 Python 语句时,程序将中止函数执行,并将产生的值返回给调用方。当函数被挂起时,该函数的状态将被保存,其中包括诸如任何变量绑定,指令指针,内部堆栈和任何异常处理之类的数据。再次调用生成器时,将恢复状态,并且函数从 yield 命中的最后一条语句继续执行,就像以前的 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

第一次执行该函数时,执行指针位于开头,因此我们在第 yield 2 行单击了第一个指针,因此在屏幕上打印了“我第一次来这里”语句。第二次 next() 被调用,执行指针从第 3 行继续,yield 在第 4 行命中第二条语句并返回“我的第二次在这里”,尽管从技术上讲,它只在该行中一次。现在 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

嗯……那没有按计划进行。问题出在我们实现的生成器函数中。为了使用 send 方法,我们需要进行一些更改,使其看起来像这样:

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 应用程序的性能非常有用,特别是在使用大型数据集或文件的情况下。通过避免复杂的迭代器实现或通过其他方式自行处理数据,它们还将使代码更清晰。


标题:如何在Python中使用Generator和yield
作者:给我丶鼓励
地址:https://blog.doiduoyi.com/articles/1591800007249.html

评论

发表评论