在Python中使用生成器来节省大量内存
当内存管理和维护生成的值之间的状态成为程序员的一项艰巨工作时, Python实现了一个名为Generators的友好解决方案。
使用生成器,函数会演变为分段访问和计算数据。因此,函数可以根据请求将结果返回给调用者,并可以维护其状态。生成器通过在向调用者生成值后暂停代码来维护函数状态,并根据请求从中断处继续执行。
由于生成器按需访问和计算值,因此不需要将大量数据完全保存在内存中,从而节省大量内存。
生成器语法
当一个函数在代码中有一个 yield 语句时,我们可以说它是一个生成器。就像在 return 语句中一样,yield 语句也向调用者发送一个值,但它不会退出函数的执行。相反,它会暂停执行,直到收到下一个请求。根据请求,生成器从停止的地方继续执行。
def primeFunction():
prime = None
num = 1
while True:
num = num + 1
for i in range(2, num):
if(num % i) == 0:
prime = False
break
else:
prime = True
if prime:
# yields the value to the caller
# and halts the execution
yield num
def main():
# returns the generator object.
prime = primeFunction()
# generator executes upon request
for i in prime:
print(i)
if i > 50:
break
if __name__ == "__main__":
main()
输出
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
与发电机通讯
调用者和生成者如何相互通信?在这里,我们将讨论Python中的 3 个内置函数。他们是:
- 下一个
- 停止迭代
- 发送
下一个
next函数可以向生成器请求其下一个值。根据请求,生成器代码将执行,yield 语句将值提供给调用者。此时,生成器停止执行并等待下一个请求。让我们通过考虑斐波那契函数来深入挖掘。
def fibonacci():
values = []
while True:
if len(values) < 2:
values.append(1)
else :
# sum up the values and
# append the result
values.append(sum(values))
# pop the first value in
# the list
values.pop(0)
# yield the latest value to
# the caller
yield values[-1]
continue
def main():
fib = fibonacci()
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3
print(next(fib)) # 5
if __name__ == "__main__":
main()
输出
1
1
2
3
5
- 通过调用 fibonacci函数并将其返回值保存到 fib 来创建生成器对象。在这种情况下,代码还没有运行, Python解释器识别生成器并返回生成器对象。由于该函数有一个 yield 语句,所以生成器对象被返回而不是一个值。
fib = fibonacci() fib
输出generator object fibonacci at 0x00000157F8AA87C8
- 使用 next 函数,调用者向生成器请求一个值并开始执行。
next(gen)
输出1
- 由于值列表为空,因此执行“if 语句”中的代码并将“1”附加到值列表中。接下来,使用 yield 语句将值让给调用者并停止执行。这里要注意的一点是在执行 continue 语句之前执行暂停。
# values = [] if len(values) < 2: # values = [1] values.append(1) # 1 yield values[-1] continue
- 在第二次请求时,代码从停止的地方继续执行。在这里,它从 `continue` 语句执行并将控制权传递给 while 循环。
现在值列表包含来自第一个请求的值。由于“值”的长度为 1 且小于 2,因此“if 语句”中的代码将执行。# values = [1] if len(values) < 2: # values = [1, 1] values.append(1) # 1 (latest value is provided # to the caller) yield values[-1] continue
- 同样,使用 next(fib) 请求该值,并从 `continue` 语句开始执行。现在值的长度不小于2。因此它进入else语句并对列表中的值求和并附加结果。 pop 语句从列表中删除第一个元素并产生最新的结果。
# values = [1, 1] else: # values = [1, 1, 2] values.append(sum(values)) # values = [1, 2] values.pop(0) # 2 yield values[-1] continue
- 您对更多值的请求将重复该模式并产生最新值
停止迭代
StopIteration 是用于退出生成器的内置异常。当生成器的迭代完成时,它会通过引发 StopIteration 异常向调用者发出信号并退出。
下面的代码解释了这个场景。
def stopIteration():
num = 5
for i in range(1, num):
yield i
def main():
f = stopIteration()
# 1 is generated
print(next(f))
# 2 is generated
print(next(f))
# 3 is generated
print(next(f))
# 4 is generated
print(next(f))
# 5th element - raises
# StopIteration Exception
next(f)
if __name__ == "__main__":
main()
输出
1
2
3
4
Traceback (most recent call last):
File “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, line 19, in
main()
File “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, line 15, in main
next(f) # 5th element – raises StopIteration Exception
StopIteration
下面的代码解释了另一种情况,程序员可以提出 StopIteration 并退出生成器。
raise StopIteration
def stopIteration():
num = 5
for i in range(1, num):
if i == 3:
raise StopIteration
yield i
def main():
f = stopIteration()
# 1 is generated
print(next(f))
# 2 is generated
print(next(f))
# StopIteration raises and
# code exits
print(next(f))
print(next(f))
if __name__ == "__main__":
main()
输出
1
2
Traceback (most recent call last):
File “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, line 5, in stopIteration
raise StopIteration
StopIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, line 19, in
main()
File “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, line 13, in main
print(next(f)) # StopIteration raises and code exits
RuntimeError: generator raised StopIteration
发送
到目前为止,我们已经看到了在通信是单向的情况下,生成器如何为调用代码产生值。截至目前,生成器还没有收到来自调用者的任何数据。
在本节中,我们将讨论允许调用者与生成器通信的 `send` 方法。
def factorial():
num = 1
while True:
factorial = 1
for i in range(1, num + 1):
# determines the factorial
factorial = factorial * i
# produce the factorial to the caller
response = yield factorial
# if the response has value
if response:
# assigns the response to
# num variable
num = int(response)
else:
# num variable is incremented
# by 1
num = num + 1
def main():
fact = factorial()
print(next(fact))
print(next(fact))
print(next(fact))
print(fact.send(5)) # send
print(next(fact))
if __name__ == "__main__":
main()
输出
1
2
6
120
720
生成器根据调用者的请求(使用 next 方法)生成前三个值(1、2 和 6),第四个值(120)根据调用者提供的数据(5)生成(使用 send方法)。
让我们考虑生成器产生的第三个数据 (6)。 3 = 3*2*1 的阶乘,由生成器产生,执行停止。
factorial = factorial * i
此时,调用者使用`send`方法并提供数据'5`。因此生成器从它停止的地方执行,即将调用者发送的数据保存到`response`变量( response = yield factorial
)。由于 `response` 包含一个值,代码进入 `if` 条件并将响应分配给 `num` 变量。
if response:
num = int(response)
现在流程传递到“while”循环并确定阶乘并交给调用者。同样,生成器会暂停执行,直到下一个请求。
如果我们查看输出,我们可以看到在调用者使用 `send` 方法后订单被中断。更准确地说,在前 3 个请求中,输出如下:
1 = 1 的阶乘
2 = 2 的阶乘
3 = 6 的阶乘
但是当用户发送值 5 时,输出变为 120,而 `num` 保持值 5。在下一个请求(使用 `next`)中,我们希望 num 会根据上一个 `next` 请求(即 3+1 = 4) 而不是 `send` 方法。但在这种情况下,`num` 增加到 6(基于使用 `send` 的最后一个值)并产生输出 720。
下面的代码显示了处理调用者发送的值的不同方法。
def factorial():
num = 0
value = None
response = None
while True:
factorial = 1
if response:
value = int(response)
else:
num = num + 1
value = num
for i in range(1, value + 1):
factorial = factorial * i
response = yield factorial
def main():
fact = factorial()
print(next(fact))
print(next(fact))
print(next(fact))
print(fact.send(5)) # send
print(next(fact))
if __name__ == "__main__":
main()
输出
1
2
6
120
24
标准库——生成器
- 范围
- dict.items
- 压缩
- 地图
- 文件对象
范围
Range函数返回一个可迭代的范围对象,它的迭代器是一个生成器。它返回从下限开始一直持续到达到上限的顺序值。
def range_func():
r = range(0, 4)
return r
def main():
r = range_func()
iterator = iter(r)
print(next(iterator))
print(next(iterator))
if __name__ == "__main__":
main()
输出
0
1
dict.items
Python中的 Dictionary 类提供了三种可迭代的方法来迭代字典。它们是键、值和项,它们的迭代器是生成器。
def dict_func():
dictionary = {'UserName': 'abc', 'Password':'a@123'}
return dictionary
def main():
d = dict_func()
iterator = iter(d.items())
print(next(iterator))
print(next(iterator))
if __name__ == "__main__":
main()
输出
('UserName', 'abc')
('Password', 'a@123')
压缩
zip 是一个内置的Python函数,它接受多个可迭代对象并一次迭代。它们从每个迭代中产生第一个元素,然后是第二个元素,依此类推。
def zip_func():
z = zip(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
return z
def main():
z = zip_func()
print(next(z))
print(next(z))
print(next(z))
if __name__ == "__main__":
main()
输出
('a', 1)
('b', 2)
('c', 3)
地图
map函数将函数和可迭代对象作为参数,并将函数的结果计算到可迭代对象的每个项。
def map_func():
m = map(lambda x, y: max([x, y]), [8, 2, 9], [5, 3, 7])
return m
def main():
m = map_func()
print(next(m)) # 8 (maximum value among 8 and 5)
print(next(m)) # 3 (maximum value among 2 and 3)
print(next(m)) # 9 (maximum value among 9 and 7)
if __name__ == "__main__":
main()
输出
8
3
9
文件对象
即使文件对象有一个 readline 方法来逐行读取文件,它也支持生成器模式。一个区别是这里的 readline 方法会捕获 StopIteration 异常,并在到达文件末尾时返回一个空字符串,这与使用 next 方法时不同。
使用 next 方法时,文件对象产生包括换行符 (\n)字符的整行
def file_func():
f = open('sample.txt')
return f
def main():
f = file_func()
print(next(f))
print(next(f))
if __name__ == "__main__":
main()
输入:sample.txt
Rule 1
Rule 2
Rule 3
Rule 4
输出
Rule 1
Rule 2
发电机用例
生成器的基本概念是按需确定值。下面我们将讨论源自上述概念的两个用例。
- 分段访问数据
- 分段计算数据
分段访问数据
为什么我们需要分段访问数据?当程序员必须处理大量数据时,这个问题是有效的,比如读取文件等等。在这种情况下,复制数据并对其进行处理不是可行的解决方案。通过使用生成器,程序员可以一次访问一个数据。在考虑文件操作时,用户可以逐行访问数据,如果是字典,则一次访问两个元组。
因此,生成器是处理大量数据的重要工具,可避免不必要的数据存储并节省大量内存。
分段计算数据
编写生成器的另一个原因是它能够根据请求计算数据。从上面的斐波那契函数可以看出,生成器是按需生成值的。此过程避免了不必要的计算和存储值,因此可以提高性能并节省大量内存。
需要注意的另一点是生成器计算无限数量数据的能力。
生成器委托
生成器可以像函数一样调用另一个生成器。使用“yield from”语句,生成器可以实现这一点,该过程称为生成器委托。
由于生成器正在委托给另一个生成器,因此发送到包装生成器的值将对当前的委托生成器可用。
def gensub1():
yield 'A'
yield 'B'
def gensub2():
yield '100'
yield '200'
def main_gen():
yield from gensub1()
yield from gensub2()
def main():
delg = main_gen()
print(next(delg))
print(next(delg))
print(next(delg))
print(next(delg))
if __name__ == "__main__":
main()
输出
A
B
100
200
概括
对于处理大量数据的程序员来说,生成器是必不可少的工具。它能够按需计算和访问数据,从而提高性能和节省内存。此外,当需要表示无限序列时,请考虑使用生成器。