生成器

用法

一个 常用的 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())
# 

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

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 有更多的优点,这里不再赘述。

你可能感兴趣的:(生成器)