Python生成器
关于生成器的一点笔记
概念
生成器,其本质是在数据需要的时候数据才出现,其他时候待命就好。
如此一样,原本大块的数据,得并非同时需要的数据,是可以被分割成一块块的小数据,这样减去了数据所占用内存空间,也不用消耗计算机提前计算一大块数据的时间。
使用yield
语句构建生成器,书上说的使用StopIteration
异常来终止生成器,并且不会生成回溯,仅仅是在python2
的解释器上。
在python3.7
中,StopIteration
会生成回溯,并提示错误RuntimeError: generator raised StopIteration
而同一份代码在python2
则会不报错。
另外,在pyton2
中不能使用return
来终止生成器,并且还会报错。而在python3
中可以使用return
来终止生成器,不报错。
建议使用StopIteration
来终结生成器,因为其只在python3.7
中才会报错,即3+
版本还是能正常运行的。如果是使用了循环,那可是用if else
和break
语句搭配来终止生成器,其可在版本2和版本3中运行。
使用示例
demo1
def generation():
yield 1
yield 2
yield 3
yield 4
if __name__ == '__main__':
a = [i for i in generation()]
print(a) # 运行结果 [1, 2, 3, 4]
demo2
生成器是迭代器的一种,迭代对象则是任何定义了__iter__
方法的对象。
可迭代对象的__iter__
方法负责返回一个迭代器。
Python中的迭代器则是包含__next__
方法的任何对象,因此,它们能响应next
函数
生成器可以是迭代器,但不一定是迭代对象
In [2]: def generation():
...: yield 1
...: yield 2
...:
In [3]: a = generation()
In [4]: next(a)
Out[4]: 1
In [5]: next(a)
Out[5]: 2
In [6]: next(a)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-6-15841f3f11d4> in <module>()
----> 1 next(a)
StopIteration:
如上所示,生成器是能响应next
函数的,直到没有值后自动报错提示StopIteration
。
那普通的range
函数可以吗?
In [8]: a = range(0, 5)
In [9]: next(a)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-15841f3f11d4> in <module>()
----> 1 next(a)
TypeError: 'range' object is not an iterator
In [10]: b = iter(a)
In [11]: next(b)
Out[11]: 0
In [12]: next(b)
Out[12]: 1
In [13]: a
Out[13]: range(0, 5)
In [14]: type(a)
Out[14]: range
In [15]: type(b)
Out[15]: range_iterator
In [16]: b
Out[16]: <range_iterator at 0x7f89b6b48480>
答案是:不可以。range
返回的是range
的对象,其是迭代对象,不能响应next
方法,仅能响应__iter__
方法,经过iter
处理后,便能响应next
函数了
demo3
生成器的宗旨是避免造成不必要的浪费*,无论是浪费内存,还是浪费计算时间,都是时间和空间上的一种浪费。
如果本来没有必要存在的数据还存在就是一种浪费,如下代码所示
def fibonacci():
nums = []
while True:
if len(nums) < 2:
nums.append(1)
else:
nums.append(sum(nums))
yield nums[-1]
# 斐波那契数据如果只需要最后一个数据的话,那之前就没有存在必要
# 从此来看,nums中只是不断的放数据,随着次数的增多,即无穷正数最终会令内存挂掉
def fibonacci():
nums = []
while True:
if len(nums) < 2:
nums.append(1)
else:
nums.append(sum(nums))
nums.pop(0)
yield nums[-1]
# 这是上一个版本的改良版,即nums最多只是一个包含两个数的列表
# 除于一开始两个数不谈,往后的最新值添加,nums有三个数
# 根据斐波那契数列的规律,只需要前两个数相加就足够了,那么,第一个值就可以弹出
# 从而不浪费内存空间
demo4
生成器有一个send
方法,其允许生成器的反向沟通。
因为yield
语句实际上是一个表达式,其有返回值就是None
,而send
方法给出的值则是此次yield
表达式的结果。并且,在send
方法调用的时候,yield
语句已经再次运行了一次,即当send
方法使用过后,看到的值已经是提供给send
方法计算一次之后的值了
def squares():
cursor = 1
while True:
response = yield cursor ** 2 # 1
if response is not None: # 2
cursor = int(response) # 3
else:
cursor += 1
if __name__ == '__main__':
s = squares()
print(next(s))
print(next(s))
print(s.send(7))
print(next(s))
"""
运行结果
1
4
49
64
如果注释了 print send,直接使用send(7),那么看到的值是1 4 64,send 的值会立刻影响计算的值并返回
"""
作用过程:
- 启用
send()
之后,值即该传给成器,步骤1
是这样response = 7
- 步骤
2
此次为真,cursor
值得到更新cursor = 7
,步骤3
完成,回到步骤1
- 再次执行
yield curosr ** 2
就能得到结果49
过程理解:send()---> (give - get - calc - return(next))
何时编写生成器
基本原则:通过自己的代码提前去做一系列工作或存储一堆数据是没有好处的。
分块访问数据
分块访问数据,即需要访问的数据量在,但是其并非需要对数据量的整体做操作的时候,是不需要将其这么大的数据存入内存,如文件读写,斐波那契数列一样。那么就需要将数据分开处理,每次处理一点,直到处理完毕。
分块计算数据
如序列的计算,因存在于无穷数,所以某种情况下序列是无穷的,那即不可能用列表表示,但生成器却可以,其只在用时才计算的原则将不会过多的占用计算机资源,只要在合适的情况下终止生成器即可。
特殊语法
yield from
:适用于生成器调用另一个生成器
def generation1():
yield 1
yield 2
yield 3
yield 4
yield 5
def generation2():
yield from generation1()
def generation3():
for i in generation1():
yield i
if __name__ == '__main__':
a = [i for i in generation2()]
print(a)
b = [i for i in generation3()]
print(b)
"""
运行记录:
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
generation2 的语法和 generation3 的语法所发挥的作用是一样,且 geneeration2 的讲法更精简
"""
参考书籍
pyton高级编程(作者:Luke Sneeringer)