本位目标人群是拥有Python基础的开发者,主要目的是用通俗的语言讲Python生成器函数的特性,方法,和使用。
生成器是Python高级编程中重要的知识点,我们平常调用的库其中有不少生成器的使用,但我们自己很少制作他,今天我们来自己写一个生成器并了解其工作原理。
以下是本篇文章正文内容,下面案例可供参考
定义:1,生成器也是一种迭代器,但是你只能对其迭代一次 。
2,这是因为它们并没有把所有的值存在内存中,而是在运行时生成值。你通过遍历 来使用它们,要么用一个“for”循环,要么将它们传递给任意可以进行迭代的函数和结构。大多数时候生成器是以函数来实现的。然而,它们并不返回一个值,而是yield(暂且译作“生出”)一个值。
我们来用人话解释一下这两条:
1,迭代器大家都非常熟悉了,在这里不过多赘述了,生成器是迭代器的一种,是一个可迭代的对象,但是我们只能迭代一次,也就是我们在for循环迭代的时候,每次只能够得到一个值。
2,因为生成器没有把所有值存在内存中,而是不断生成,所以它相比普通的迭代器和循环存储在列表更省内存。
通过解释我们知道,生成器是迭代器的细分,它跟迭代器有一样的迭代方法,也更省内存。我们来通过一个简单的对比来理解一下;
首先看这个例子:
# 如果我们要打印1-10这样的数据,用最常用的方法是使用range函数,range函数相当于生成了一个内容为1-10的列表
# 但是如果1-1000000呢?随着极限的增大,range生成的列表占用内存越来越大,而且这个列表的长度是不确定的
for i in range(1, 11):
print(i)
这样来打印1-10实际上就是创建了一个元素为1-10的列表,所以如果我们要取的极限太大,那列表就会很长很长,就非常的占内存。
而通过生成器就可以解决内存的问题:
# 只有当我们调用next()方法时,才会真正的从内存中取出数据,并且这个数据只能被取出一次
def get_num():
num = 0
while num < 10:
num += 1
yield num
for g in get_num():
print(g)
因为生成器每次遇到yield只产生一个值,所以它一边运算一边产生一个值,每次都覆盖,这样内存占用是不是大大减少了呢?
大家看到了关键字yield和我们用for循环迭代这个生成器函数,以及注释里的next方法,那这是啥东西呢,咱们在下个板块看。
return我们知道是返回一个值然后这个函数的运行就结束了,我们可以吧yield想象成一个return,但是不是让程序结束,而是返回一个值之后暂停。
首先咱们连着上文的例子,:
def get_num():
num = 0
while num < 10:
num += 1
return num
print(get_num())
输出:1
没错,这个函数我们在进入循环后:一,num加上了1,此时num=1 。二,return出来了num,函数结束。所以我们打印函数的返回值就是一个1
如果我们稍微改一下:
# 只有当我们调用next()方法时,才会真正的从内存中取出数据,并且这个数据只能被取出一次
def get_num():
num = 0
while num < 10:
print('循环')
num += 1
yield num
g = get_num()
print(next(g))
输出:循环
1
我们发现,这个改成yield出来值之后就是一个生成器函数,这个生成器我们把它给了g这个变量,并且调用了next方法,输出了next方法的返回值,而这个返回值刚好就是第一次循环里的num。之后程序结束
由此我们可以得知,生成器遇到yield时就会返回出一个值并且暂停函数运行,如果我们调用了next则yield后面的值就传递给了next方法,变成了next方法的返回值。之后yield这一句话就可以跳过了,回到了循环顶部,但暂停状态没有解除。
如果我们调用两次next方法:
def get_num():
num = 0
while num < 10:
print('循环')
num += 1
yield num
g = get_num()
print(next(g))
print(next(g))
输出: 循环
1
循环
2
我们发现在第二次next调用后,第二次循环才执行,同样遇到了yield,暂停函数,返回值,回到循环顶部........由此可见,next的作用就是先解除暂停,把yield这个值返回出来之后跳出,next的工作已经结束了,此时函数在yield的后一句也就是循环顶部暂停着,当下次next调用时就会解除暂停,然后返回yield,然后next工作结束程序跳过yield......
这就是next的作用,在刚刚暂停的地方开始执行,并返回yield的值
这里引用一句别的大佬「冯爽朗」的解释:
到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
接下来还有一个知识点,就是yield from,这个一般就是在生成器里调用别的生成器或自己(递归),并且yield出别的生成器里yield的值。是有点绕哦,换个理解的办法,你就吧yield from当成:
for item in get_num():
yield item
这个知识点的应用我们放在最后一个板块来看。
send方法包含了next方法,向生成器发送一个值之后就恢复运行,返回值就是yield的值。
def get_num():
num = 0
while num < 10:
num += 1
var = yield num
print(var)
g = get_num()
print(next(g))
print(g.send(1))
输出: 1
1
2
此时第一个1是next函数获得的yield返回值,但是yield给变量啥都没啊,所以现在var是None,之后跳过yield这行到了下面一行前暂停着,这个时候send来了,发了个1给var,然后执行了next,这就是第二个1,这个1是send发的,在生成器内部执行了,这个2就是send包含的next的返回结果,也是send的返回值。
接下来是close,next在遇到StopIteration异常就会停止迭代,StopIteration异常会在生成器函数体运行完毕后或者close之后抛出,close会在函数暂停的地方抛出GeneratorExit异常
GeneratorExit
异常的产生意味着生成器对象的生命周期已经结束,因此生成器方法后续语句中不能再有yield
,否则会产生RuntimeError
。
def get_num():
num = 0
while num < 10:
num += 1
yield num
g = get_num()
print(next(g))
g.close()
print(next(g))
对已经关闭的生成器对象使用 next 会抛出 StopIteration 异常。
输出:1
Traceback (most recent call last):
File "/Users/ruiyang/PycharmProjects/AnacondaProject_2/test2.py", line 11, in
print(next(g))
StopIteration
接下来是throw,throw是在生成器暂停的地方抛出一个异常,并且返回下一个yield的返回值
def get_num():
num = 0
while num < 10:
try:
num += 1
yield num
except ZeroDivisionError:
print("ZeroDivisionError")
g = get_num()
print(next(g))
print(g.throw(ZeroDivisionError))
print(next(g))
输出: 1
ZeroDivisionError
2
3
我们可以看到程序捕获到了一场,并且返回了下一个yield的值
如果大家想看更详细的方法介绍移步此文:Python 生成器与它的 send,throw,close 方法_团子大圆帅的博客-CSDN博客
讲了这么多我们也该说一下构建生成器之后该如何使用了,首先生成器是一个可迭代对象,也是一种迭代器,所以我们用迭代的方式来使用生成器:
这也就是文章开头是用for循环的原因:
# 只有当我们调用next()方法时,才会真正的从内存中取出数据,并且这个数据只能被取出一次
def get_num():
num = 0
while num < 10:
num += 1
yield num
for g in get_num():
print(g)
接下来是一些实例,首先我们从简单的来说,我们可以用生成器来求斐波那契数列,非常pythonic且不会占很多内存:
def fibo():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
for f in fibo():
print(f)
if f > 100:
f.close()
break
你没看错函数内部就是while True,我们在外部close结束掉生成器的生命周期并且停止迭代就好了。如果不想外部这么多,我们在内部修改while循环条件即可。
接下来就是递归yield的用处了,这里面用到了上文说的yield from
加入我们有一个嵌套非常非常多的列表,我们用现在网上传的普通的递归会发现如果列表长度大于N(N>3)列表就不能正常解开了,因为return的递归只能返回一次值,其中逻辑非常复杂,自己改的话也非常难理解,这个时候,生成器就完美解决了这个问题:
# 多层嵌套的列表
num_list = [1, [2, [3, 'end'], 5], 6]
# 展开嵌套列表的生成器函数(递归)
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
# yield from flatten(item)就相当于for item in flatten(item): yield item
yield from flatten(item)
else:
# 每次遇到yield函数就会返回一个值,并且暂停执行后面的代码,当函数外部调用next()函数时,就会从yield函数中恢复执行
yield item
# 遍历生成器函数时for循环底层在不断的调用next()函数,直到遇到StopIteration异常,list构造函数可以将所有可迭代对象转换成list
print(list(flatten(num_list)))
# 如果当调用next方法时生成器函数结束(遇到空的return语句或是到达函数体末尾),则这次next方法的调用将抛出StopIteration异常(即for循环的终止条件)
# 如果调用next方法时生成器函数没有结束,则这次next方法的调用将返回遇到的yield的值,并且恢复生成器继续运行(即for循环的迭代条件)
for f in flatten(num_list):
print(f)
输出: [1, 2, 3, 'end', 5, 6]
1
2
3
end
5
6
列表完美解开,各种类型都可以。
其中的逻辑就是如果不是列表就直接yield出这个值,如果是列表就调用自己,遍历列表,然后yield出列表里的值,以此类推,希望大家能够好好阅读一下代码并加以思考。
生成器python高级编程中重要的知识点,我们运用好生成器可以解决很多内存占用啊,等等的问题,希望如果阅读了感觉文章有用就请点个赞,有不足的地方也请指出。