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