ython 生成器有助于实现维护持久状态的功能。这支持增量计算和迭代。此外,可以使用生成器代替数组以节省内存。这是因为生成器不存储值,而是存储具有函数状态的计算逻辑,类似于准备触发的未计算函数实例。
生成器表达式
可以使用生成器表达式代替数组创建操作。与数组不同,生成器将在运行时生成数字。
>>> import sys
>>> a = [x for x in range(1000000)]
>>> b = (x for x in range(1000000))
>>> sys.getsizeof(a)
8697472
>>> sys.getsizeof(b)
128
>>> a
[0, 1, ... 999999]
>>> b
at 0x1020de6d0>
我们可以看到,在上面的场景中,我们通过使用生成器代替数组节省了相当多的内存。
用 yield 代替 return 的函数
让我们考虑一个简单的例子,你想生成任意数量的素数。以下是检查一个数字是否为素数的函数以及将为我们产生无限多个素数的生成器。
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 Truedef getPrimes():
value = 0
while True:
if isPrime(value):
yield value
value += 1
正如你在第二个函数中看到的,我们在 while 循环中迭代并产生质数。让我们看看如何使用上面的生成器。
primes = getPrimes()>>> next(primes)
2
>>> next(primes)
3
>>> next(primes)
5
首先,我们调用函数并获取生成器实例。虽然这可以模拟无限数组,但目前还没有找到任何元素。如果调用 list(primes),程序可能会因 MemoryError 而崩溃。但是,对于素数,它不会去那里,因为素数空间是稀疏的,计算可以在有限的时间内达到内存限制。但是,对于发电机,您不会事先知道长度。如果你调用 len(primes),你将得到以下错误,原因与数字仅在运行时生成的原因相同。
----------------------------------------------------------------
TypeError Traceback (most recent call last)
in
----> 1 len(primes)
TypeError: object of type 'generator' has no len()
迭代次数有限的生成器
尽管我们的素数示例具有无限的迭代空间,但在大多数日常场景中,我们面临的是有限计算。因此,让我们看一个示例,我们可以使用它来读取包含文本数据的文件以及下一行句子的语义分数。
为什么我们需要使用 yield?
假设文件为 1TB,单词语料库为 500000。它不适合内存。一个简单的解决方案是一次读取 2 行,计算每行的单词字典,并在下一行返回语义分数。该文件将如下所示。
The product is well packed
5
Edges of the packaging was damaged and print was faded.
3
Avoid this product. Never going to buy anything from ShopX.
1
Shipping took a very long time
2
很明显,我们不需要立即打开文件。此外,这些行必须矢量化,并可能保存到另一个文件中,该文件可以直接解析以训练机器学习模型。因此,为我们提供干净代码的选项是使用一个生成器,该生成器将一次读取两行,并将数据和语义分数作为元组提供给我们。
实现文件解析生成器
假设我们在名为 test.txt 的文件中有上述文本文档。我们将使用以下生成器函数来读取文件。
def readData(path):
with open(path) as f:
sentiment = ""
line = ""
for n, d in enumerate(f):
if n % 2 == 0:
line = d.strip()
else:
sentiment = int(d.strip())
yield line, sentiment
我们可以在 for 循环中使用上述函数,如下所示。
>>> data = readData("test.txt")
>>> for l, s in data: print(l, s)
The product is well packed 5
Edges of the packaging was damaged and print was faded. 3
Avoid this product. Never going to buy anything from ShopX. 1
Shipping took a very long time 2
生成器如何退出?
在正常的 for 循环中,当生成器不再执行 yielding 时,迭代将停止。但是,我们可以通过在生成器实例上手动调用 next() 来观察到这一点。在迭代限制之外调用 next() 将引发以下异常。
----------------------------------------------------------------
StopIteration Traceback (most recent call last)
in
---> 28 print(next(data))StopIteration:
使用 send、throw 和 close
send 函数
让我们回顾一下我们的素数示例。想象一下,我们想将生成器函数的值重置为 100,如果它们是质数,则开始产生大于 100 的值。我们可以在生成器实例上使用 send() 方法将值推送到生成器中,如下所示。
>>> primes = getPrimes()
>>> next(primes)
2
>>> primes.send(10)
11
>>> primes.send(100)
101
请注意,在调用 send() 之前,我们必须至少调用 next() 一次。让我们看看如何修改我们的函数以适应目的。因为函数应该知道如何分配接收到的值。
def getPrimes():
value = 0
while True:
if isPrime(value):
i = yield value
if i is not None:
value = i
value += 1
我们将 yield 值存储在变量 i 中。如果不是 None 类型,我们将其分配给 value 变量。None 检查是必不可少的,因为第一个 next() 在 value 变量中没有 value to yield。
throw 函数
假设您希望以大于 10 的值结束迭代,以避免溢出或超时(假设)。throw() 函数可用于提示生成器停止引发异常。
primes = getPrimes()for x in primes:
if x > 10:
primes.throw(ValueError, "Too large")
print(x)
此技术可用于验证输入。逻辑在于生成器的用户。这将产生以下输出。
2
3
5
7----------------------------------------------------------------
ValueError Traceback (most recent call last)
in
12 for x in primes:
13 if x > 10:
---> 14 primes.throw(ValueError, "Too large")
15 print(x)
in getPrimes()
3 while True:
4 if isPrime(value):
----> 5 i = yield value
6 if i is not None:
7 value = i
ValueError: Too large
close 函数
手头无一例外地处理瓶盖通常是优雅的。在这种情况下,close() 函数可用于有效地关闭迭代器。
primes = getPrimes()for x in primes:
if x > 10:
primes.close()
print(x)
这将得到以下输出。
2
3
5
7
11
请注意,我们的值为 11,这是最后一个计算出的大于 11 的值。这模拟了 C/C++ 中 do while 循环的行为。