Python yield 与生成器

来源:互联网 发布:淘宝无线端主图大小 编辑:程序博客网 时间:2024/05/16 05:29
 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 函数应接不暇, 这样会有诡异的同步问题. 所以正确的语义应该是
  • 当生成器函数执行到包含 yield 的表达式时, 函数挂起, 并将 yield 之后的表达式作为返回值传递给next 函数调用
  • 下一次 next 函数调用又会驱动该生成器的函数体继续执行此后的语句, 直到遇见下一个 yield 再次挂起并生成一个值交给 next
  • 如果某次 next 调用驱动了生成器继续执行, 而此后函数正常结束的话, 那么不会有任何值传递给 next 函数, 同时, 生成器会抛出 StopIteration 异常
    对于最后一点, 下面这个例子可作为参考
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.
    不过话说回来, 能手动控制让执行到一半的函数挂起, 等到之后再执行, 还是挺有意思的. (完)
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 淘宝卖家不同意退款怎么办 淘宝仅退款卖家不处理怎么办 淘宝申请退款卖家不处理怎么办 快递没收到货要退款怎么办 发票给了不给钱怎么办 付款后不给发票怎么办 供货商不给发票怎么办 刚生过孩子太胖买衣服怎么办 黑色牛仔裤洗的发白怎么办 蘑菇街手机丢了怎么办 黑衣服上全是白毛毛怎么办 支付宝注销了钱怎么办 网上买东西手机号写错了怎么办 二类工资卡过万怎么办 淘宝发货地址写错怎么办 淘宝发货地址写错了怎么办 淘宝不能代付了怎么办 支付宝付款码被盗刷怎么办 地方选举追究不到相关责任人怎么办 天正建筑画个直线找不到怎么办 Wi-Fi模块不支持多播怎么办? 魅族手机屏幕点不动怎么办 uc打开网页很慢怎么办 京东手机号码无法登录怎么办 织梦系统网站没收录怎么办 电脑开机出现一堆乱码怎么办 电脑文件夹出现乱码打不开怎么办 电脑出现f1和f2怎么办 电脑中韩文内容显示乱码怎么办 入驻shopee没身份证怎么办 液相色谱柱柱压降低怎么办 c18色谱柱堵了怎么办 色谱柱进空气了怎么办 宫颈评分只有3分怎么办 淘宝鞋子售后退货商家拒收怎么办 退货申通cp原因怎么办 运费险赔的少怎么办 淘宝卖游戏账号恶意退款怎么办 淘宝账号体检虚拟违规怎么办 京东虚拟单被骗怎么办 网络公选课挂科怎么办