生成器

作者: l1n3x | 来源:发表于2020-03-03 15:20 被阅读0次

    用法

    一个 常用的 yield 应用场景是使用它生成斐波那契数列,代码如下:

    def fib():
        a, b = 0, 1
        while True:
            yield b
        a, b = b, a + b
    

    当我们调用 fib 时并不是直接获得函数的返回值,而是获得一个生成器对象:

    def fib():
        a, b = 0, 1
        while True:
            yield b
        a, b = b, a + b
    print(fib())
    # <generator object fib at 0x030C30B0>
    

    一种简单的使用生产器对象方式为循环:

    def fib(n):
        a, b = 0, 1
        while True:
            yield b
            if b >= n:
                return
            a, b = b, a + b
    for x in fib(5):
        print(x)
    # 1, 1, 2, 3, 5
    

    这里我把迭代器最大的值限制在5,避免程序无休止运行下去。此外,还可以采用 next 方法来获取生成器的值:

    g = fib(5)
    print(next(g))  # 1
    print(next(g))  # 1
    print(next(g))  # 2
    print(next(g))  # 3
    print(next(g))  # 5
    print(next(g))  # StopIteration
    

    可以看出第 6 次调用抛出了 StopIteration。如果一个函数已近包含 yield, 那么这个函数在执行 return 语句时即会抛出 StopIteration 异常,且返回值在异常对象的 value 属性中。例如:

    def gen():
        yield 5
        return "end"
    g = gen()
    next(g)
    try:
        next(g)
    except StopIteration as e:
        print(e.value) # end
    

    当然,以下这种情况依然会抛出 StopIteration,只是最终的返回值为 None 而已:

    def gen():
        yield 5
    g = gen()
    next(g)
    try:
        next(g)
    except StopIteration as e:
        print(e.value) # None
    

    为什么需要 yield

    在了解 yield 的基本用法后,还需要知道为什么需要使用 yield。如果没有 yield,我们当然可以使用 list 来保存数列的值:

    def fiblist(n):
        res = []
        a, b = 0, 1
        while b <= n:
            res.append(b)
            a, b = b, a + b
        return res
    print(fiblist(5)) # [1, 1, 2, 3, 5]
    

    可以看出,使用 list 同样可以计算数列。但是这时存在一个问题,如果我需要生成大量的数列这时就存在两个问题:

    1. 在生成数列的同时我们需要进行一次循环,而从 list 里面取出数列时我们同样还要循环一次。增加了循环的成本。
    2. 所有生成的数都保存在内存中,如果后续依旧采用这种方式会急速的使得内存浪费。

    其实者涉及到代码设计中的一种使用资源的方式: 延迟加载。即资源并不在它声明的时候马上读取到内存中,而是当真正需要使用的时候菜才进行加载。

    而当采用 yield 生产数列的时候则是符合这一观点的,即如果你不使用循环或者 next 去读取生成器,则它永远只是一个生成器对象,并不会发生真正的计算。

    为什么yield能行

    许多解析 yield 的文章都止步于前两个内容,而并未分析为什么 yield 能行。yield 最关键的思想在于当我们 "调用" 一次含有 yield 的函数之后,函数的上下文依然存在。即 yield 的真正作用为在函数在切换到其他上下文,依然会保存自己的上下文。例如对于 fib 函数,当我们调用一次 next 后,a, b 的值未变为初始值,而 是 yield 之前的值。这有一点类似于 c 语言中的静态函数。

    yield 还能做什么

    利用上面提到的特点,yield 还可以完成三个基础操作:

    1. 保存上下文,进行迭代计算
    2. 保存上下文,缓存资源
    3. 保存上下文,使得异步更加优雅

    其中计算斐波那契数列则是上面的第一点应用。至于第二点应用,还需要提到学习一下生成器的其他用法。

    send

    生成器除了使用 next 函数取值以外,还可以使用 send 函数向生成器传值并获得一个返回值,例如:

    def gen():
        b = yield "start"
        while True:
            c = yield b
            if c == None:
                return "end"
            b = c
    
    g = gen()
    a = g.send(None)
    print(a)  # start
    print(g.send(1))  # 1
    print(g.send(2))  # 2
    print(g.send(3))  # 3
    print(g.send(None))  # StopIteration value = end
    

    python 规定,第一次向生成器发送的值必需为 None,起作用为启动生成器。上面函数的运行流程为:

    1. 调用 a = g.send(None)。gen 运行到 yield "start",gen 返回 "start"。
    2. 调用 a.send(1)。gen 函数继续运行,此使获取到 send 发送的值为 1,则 b = 1。gen函数继续运行到 yield b, 此使gen 函数返回 b,即1。
    3. 调用a.send(2)。gen 函数继续运行,此时获取到 c 的值为 2,继续运行,b = c = 2,遇到下一个 yield ,返回2。

    其实 a = yield b 这条语句可以看作两个部分: 第一步为 yield 即函数先返回 b。此时函数停止运行,等待send。当下一个 send 调用产生后,函数继续运行并将接收的值传递给 a。

    有了这个知识后。考虑这样一个需求: 接收一个正则表达式,并判定某个字符串是否被该表达式匹配。一般来说可以这样写:

    class Re:
        def __init__(self, reg_expr):
            self._reg_expr = re.compile(reg_expr)
    
        def test(self, string):
            return bool(self._reg_expr.match(string))
    r = Re("hello")
    print(r.test("hello world"))  # True
    print(r.test("python"))  # False
    

    但是有了 yield 之后,我们则可以这样写:

    def re_test(re_expr):
        cached_re = re.compile(re_expr)
        sentence = yield None
        while True:
            sentence = yield bool(cached_re.match(sentence))
    test = re_test("hello")
    test.send(None)
    print(test.send("hello world"))  # True
    print(test.send("python"))  # False
    

    虽然这相比使用类实现的方式并没有节省代码,但可以更清楚的理解 yield 的保存上下文的功能。

    此外,第三个作用将在下一篇文章中介绍。

    yield from

    具有 yield 的函数虽然被当成生成器对象对待。但在开发中依然希望具有 yield 的函数能像普通函数一样工作。 对于一个普通函数,进行函数嵌套是最基础的操作,非常简单也非常好理解。例如在函数 a 中调用 函数 b:

    def a():
        b()
    

    但对于一个生成器对象来说,并不如此简单。例如一个生成器对象为 a:

    def a():
        for i in range(10):
            yield i
    

    另一个生成器 b 想调用生成器 a,并在结束后做点其他事情。这时我们并不能:

    def b():
        a()
        yield "ok"
    

    因为 a 是一个生成器对象,因此我们需要:

    def b():
        for i in a():
            yield i
        yield "ok"
    

    这就与普通的函数调用存在差异了。这时即可以使用 yield from,上面的代码等价于:

    def b():
        yield from a()
        yield "ok"
    

    是不是更加优雅了?此外,如果 a 生成器还需要接收数据,例如:

    def a():
        b = yield None
        while True:
            print(b)
            b = yield
            if b == None:
                return "end"
    

    那如果不使用 yield from, b 函数将变得异常复杂:

    def b():
        g = a()
        g.send(None)
        c = yield None
        while True:
            try:
                g.send(c)
            except StopIteration as e:
                print(e)
            c = yield
    x = b()
    x.send(None)
    x.send(1)  # 1
    x.send(2)  # 2
    x.send(None)  # end
    

    实际上,如果使用 yield from,我们只需要:

    def b():
        yield from a()
    x = b()
    x.send(None)
    x.send(1)  # 1
    x.send(2)  # 2
    

    可以看出,yield from 使得生成器的嵌套更加的优雅。此外,yield from 有更多的优点,这里不再赘述。

    相关文章

      网友评论

          本文标题:生成器

          本文链接:https://www.haomeiwen.com/subject/jzhhlhtx.html