About
RSS

Bit Focus


小记 Python yield 与生成器

Posted at 2012-12-06 10:33:41 | Updated at 2018-02-21 23:17:15

    yield 是 Python 中的一个关键字, 这个关键字比较特殊, 用于在任何表达式前, 但它不仅会对其后的表达式有影响, 对整个函数上下文都有影响. 实际上, 凡是在函数体中出现了 yield 关键字, Python 都会对此函数特殊处理, 调用这个函数不再返回值, 而是一个生成器对象.
    比如
def f():
    yield 1

g = f()
print type(g)
其结果是
<type 'generator'>
    如果要获取产生器产生的值, 则需要调用产生器对象的 next 函数
def f():
    yield 1

g = f()
print g.next()
    如果在函数中, 出现多个 yield, 或者函数的执行过程中反复路过某个 yield, 那么 next 每次调用会得到下一个产生的值, 比如
def f():
    yield 1
    yield 2

g = f()
print g.next(),
print g.next()
    得到的会是 1 2. 或如下的循环
def f():
    for x in range(3):
        yield x * 2

g = f()
print g.next(),
print g.next(),
print g.next()
    该循环中反复产生值, 每次产生一个就被传递给 next 函数作为返回值... 当然这样来说是不准确地, 如果上面 for 循环如果跑得太快, 那样会疾速产生值导致 next 函数应接不暇, 这样会有诡异的同步问题. 所以正确的语义应该是
    对于最后一点, 下面这个例子可作为参考
def f():
    yield 1

g = f()
print g.next(),
print g.next()
    另外生成器中如果出现 return, 它必须是一句空返回, 即 return 之后不允许跟任何表达式, 这也是限制之一.

    从下面这个例子可以更好地窥探生成器的执行模式
def f():
    print 'a'
    yield 1
    print 'b'
    yield 2
    print 'c'

g = f()
print 'start'
print g.next()
print 'next'
print g.next()
print 'end'
    输出结果为
start
a
1
next
b
2
end
    也就是说当第一次调用了 next 之后, f 函数体的执行就挂起了, 等着下一次调用 next. 另外, f 中最后一句应该输出的 c 并没有输出, 因为第二次生成器给出了 2 这个值之后就一直处于挂起状态.
    如果要大慈大悲地让最后一句也执行, 那么需要再加一句 next 调用
def f():
    print 'a'
    yield 1
    print 'b'
    yield 2
    print 'c'

g = f()
print 'start'
print g.next()
print 'next'
print g.next()
print 'end'
g.next()
    然而如果这样的话, c 输出之后, 还会带出个异常. 所以这东西有时候并不那么好用.

    那它有何用?
    一个典型的非它不可的场景是模拟无穷列表, 比如
def f():
    n = 0
    while True:
        yield 'loli #' + str(n)
        n = n + 1

g = f()
print g.next()
print g.next()
# ...
    运用生成器就可以构造一个用来无线生产 loli 的邪恶机器. 但是这种应用场景实在是太少了. 更多的是, 这样可以延迟计算一些值或者避免计算某些值; 另外, 将一个执行到一半的函数挂起, 然后先去做做另外一件事情, 过个十年八年再回来继续执行这个函数, 这样很有异步感的事情也可以通过 yield 来搞定.

    然而 yield 这东西并不是一个 return 的替代品, 它更像一个前置单目运算操作符, 并且因此它可以出现在表达式的任何位置. 如
def f():
    x = 'lo'
    y = 'li'
    z = (yield x) + (yield y)
    print z

g = f()
print g.next(),
print g.next()
    这段代码会输出 lo li 这个字符串, 但留下了一些疑点, 上面的含 yield 的表达式如 (yield x) 它的取值是多少? 要知道这个, 就得让代码继续运行下去然后输出变量 z 来观察. 这样的话就再加上一句 next
def f():
    x = 'lo'
    y = 'li'
    z = (yield x) + (yield y)
    print z

g = f()
print g.next(),
print g.next()
g.next()
    结果是抛出异常, 但并不是上次那个 StopIteration, 而是
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
    这么说来的话 (yield x)(yield y) 这两个表达式的值都是 None 了.

    原因是, 调用 next 函数只能从生成器中获取一个值, 而不能将一个值传入生成器中. 而 yield 这货不仅是用来从生成器传出一个值的工具, 它还是一个向生成器中 yield 所在表达式中传入值的通道. 要利用这样的通道需要调用生成器的 send 函数, 如
def f():
    x = 'lo'
    y = 'li'
    z = (yield x) + (yield y)
    print z

g = f()
print g.next()
print g.send('kagami ')
g.send('tukasa')
    以上代码仍然会得到一个异常, 不过是 StopIteration. 并且, 在此之前可以观察到 z 的值, 为通过两次 send 函数传入的字符串相连的结果.
    而更准确地说, next() 调用相当于一次 send(None) 调用, 所以在之前那个例子中会出现尝试将两个 None 值加和的错误. 因此上述代码又等价于
def f():
    x = 'lo'
    y = 'li'
    z = (yield x) + (yield y)
    print z

g = f()
print g.send(None)
print g.send('kagami ')
g.send('tukasa')
    话说仔细看这个例子还是挺凌乱的, 因为函数 f 明明内部只有两个 yield, 却有三次 send; 而第一次 send 的实参值必须是 None (否则挂), 并且最后一次 send 之后必然会出现一次 StopIteration. 这看起来简直就是 Python 生成器设计上的 bug.
    不过话说回来, 能手动控制让执行到一半的函数挂起, 等到之后再执行, 还是挺有意思的. (完)

Post tags:   Python  yield  Generator

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site