Python yield 详解(嚼碎了喂你,一篇精通,无需再看其他文章)

Python yield详解

由“斐波那契”深入理解yield案例

​ 斐波那契(Fibonacci)數列是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到

第一个版本

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        print b
        a, b = b, a + b
        n = n + 1

执行 fab(5),我们可以得到如下输出:

>>> fab(5)
1
1
2
3
5

第二个版本

提高 fab 函数的可复用性,最好不要直接打印出数列,而是返回一个 List。

def fab(max): 
    n, a, b = 0, 0, 1 
    L = [] 
    while n < max: 
        L.append(b) 
        a, b = b, a + b 
        n = n + 1 
    return L 

可以使用如下方式打印出 fab 函数返回的 List:

>>> for n in fab(5):
...     print(n)
...
1
1
2
3
5

问题的引出

稍微高级的代码的 fab 函数通过返回 List 能满足复用性的要求,但是更有经验的开发者会指出,该函数在运行中占用的内存会随着参数 max 的增大而增大,如果要控制内存占用,最好不要用 List 来保存中间结果,而是通过 iterable 对象来迭代

第三个版本

利用 iterable 我们可以把 fab 函数改写为一个支持 iterable 的 class

class Fab():
    """
    Python3:自定义迭代器的类需要同时实现__iter__和__next__方法。
    Python2:自定义迭代器的类需要同时实现__iter__和next方法。
    """

    def __init__(self, max):
        self.max = max
        self.n, self.a, self.b = 0, 0, 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.n < self.max:
            r = self.b
            self.a, self.b = self.b, self.a + self.b
            self.n = self.n + 1
            return r
        raise StopIteration()


for n in Fab(5):
    print(n)

"""
 也可以使用:Fab类通过 next() 不断返回数列的下一个数,内存占用始终为常数:
 f = Fab(5)
 print(next(f))
 print(next(f))
 print(next(f))
 print(next(f))
 print(next(f))
"""
1
1
2
3
5

​ 然而,使用 class 改写的这个版本,代码远远没有第一版的 fab 函数来得简洁。如果我们想要保持第一版 fab 函数的简洁性,同时又要获得 iterable 的效果,yield 就派上用场了。

第四个版本

​ 第四个版本的 fab 和第一版相比,仅仅把 print b 改为了 yield b,就在保持简洁性的同时获得了 iterable 的效果。

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        # print b
        a, b = b, a + b
        n = n + 1


for n in fab(5):
    print(n)
1
1
2
3
5

总结

细化总结

​ 简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。

​ 也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:

>>> f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

​ 当函数执行结束时,generator 自动抛出 StopIteration 异常,表示迭代完成。在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。

归纳总结

我们可以得出以下结论:

​ 一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

​ yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

引深

​ 要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别

如何判断一个def是否是一个特殊的 generator 函数?

可以利用 isgeneratorfunction 判断:

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        # print b
        a, b = b, a + b
        n = n + 1

from inspect import isgeneratorfunction
print(isgeneratorfunction(fab))  # True

类的定义和类的实例

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        # print b
        a, b = b, a + b
        n = n + 1

import types
print(isinstance(fab, types.GeneratorType))  # False
print(isinstance(fab(5), types.GeneratorType))  # True

fab 是无法迭代的,而 fab(5) 是可迭代的

每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:

>>> f1 = fab(3)
>>> f2 = fab(5)
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 2
>>> print 'f2:', f2.next()
f2: 2
>>> print 'f2:', f2.next()
f2: 3
>>> print 'f2:', f2.next()
f2: 5

另一个例子

​ 另一个 yield 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取。

def read_file(fpath):
    BLOCK_SIZE = 1024
    with open(fpath, 'rb') as f:
        while True:
            block = f.read(BLOCK_SIZE)
            if block:
                yield block
            else:
                return



另一个角度理解yield案例

理解

​ 先把yield看做return,这个是直观的,它首先是个return,普通的return是什么意思,就是在程序中返回某个值,返回之后程序就不再往下运行了。看做return之后再把它看做一个是生成器(generator)的一部分(带yield的函数才是真正的迭代器),好了,如果你对这些不明白的话,那先把yield看做return,然后直接看下面的程序,你就会明白yield的全部意思了

示例1

代码

def foo():
    print("starting...")
    while True:
        res = yield 4
        print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))
starting...
4
********************
res: None
4

解释

解释代码运行顺序,相当于代码单步调试:

  1. 程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)

  2. 直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环

  3. 程序遇到yield关键字,然后把yield想想成return , return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果 , 第二个是return出的结果)是执行print(next(g))的结果,

  4. 程序执行print("*"*20),输出20个*

  5. 又开始执行下面的print(next(g)) , 这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候res赋值是None,所以接着下面的输出就是res:None

  6. 程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,print函数输出的4就是这次return出的4 ,所以不管执行多少次print(next(g)),res都是None

小结

​ 到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。

示例2

代码

def foo():
    print("starting...")
    while True:
        res = yield 4
        print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(g.send(7))

这个例子就把上面那个例子的最后一行换掉了,输出结果:

starting...
4
********************
res: 7
4

解释

​ 先大致说一下send函数的概念:此时你应该注意到这次res的值变成了7,这是因为,send是发送一个参数给res的,因为上面讲到,return的时候,并没有把4赋值给res,下次执行的时候只好继续执行赋值操作,只好赋值为None了

而如果用send的话,开始执行的时候,先接着上一次(return 4之后)执行,先把7赋值给了res,然后执行next的作用,遇见下一回的yield,return出结果后结束。

解释代码运行顺序,相当于代码单步调试:

  1. 程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)

  2. 直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环

  3. 程序遇到yield关键字,然后把yield想想成return , return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果 , 第二个是return出的结果)是执行print(next(g))的结果,

  4. 程序执行print("*"*20),输出20个*

  5. 程序执行g.send(7),程序会从yield关键字那一行继续向下运行,send会把7这个值赋值给res变量

  6. 由于send方法中包含next()方法,所以程序会继续向下运行执行print方法,然后再次进入while循环

  7. 程序执行再次遇到yield关键字,yield会返回后面的值4后,程序再次暂停,直到再次调用next方法或send方法。

  • 19
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Python中的yield是一个关键字,用于在生成器函数中产生一个值,并暂停函数的执行。通过使用yield,函数可以在每次迭代时生成一个值,而不是一次性返回所有结果。这使得生成器函数能够有效地处理大量的数据或无限序列。 引用中的代码展示了一个简单的生成器函数,它通过yield语句返回一个值,并在每次迭代时暂停函数的执行。通过使用yield,我们可以逐个打印函数返回的值。 引用中的代码展示了yield语句的更多用法。在生成器函数中,yield语句可以同时接收和发送值。通过在生成器对象上调用send()方法,可以将值发送回生成器函数并继续执行函数的剩余部分。 需要注意的是,yield语句不仅仅是一个简单的return语句。它允许函数在后续调用时从上一次暂停的地方继续执行,并保留函数的状态。 总结来说,yieldPython中用于创建生成器函数的关键字,它可以在函数执行期间产生值并暂停函数的执行。通过使用yield,我们可以有效地处理大量的数据或无限序列。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [pythonyield的用法详解——最简单,最清晰的解释](https://blog.csdn.net/mieleizhi0522/article/details/82142856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值