前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python-yield关键字详解

Python-yield关键字详解

作者头像
Kevinello
发布2022-08-19 11:12:38
5510
发布2022-08-19 11:12:38
举报
文章被收录于专栏:Kevinello的技术小站

前言

yield这个关键字很早的时候就了解过,但一直都只了解其基本使用,即转变函数为生成器的使用,节省大型迭代时的内存空间,但其实yield在python的很多特性中都起着重要的作用

这篇文章就详细展开一下yield关键字

需要了解的几个词

  • 容器(container):python中容器指一个用来存储多个元素的数据结构,常见的list,tuple,dict,set,string都是容器
  • 可迭代对象(iterable):在python中能被迭代获取的对象,iterable的对象一定实现了__iter__()方法
  • 迭代器(iterator):迭代器实现了__iter__()__next__()方法,是一个带状态的对象,迭代器内部持有一个状态,该状态用于记录**当前迭代所在位置,**以便于下次迭代的时候获取正确的元素,迭代器可以通过next()方法来迭代获取下一个值
  • 生成器(generator):生成器是一种特殊的迭代器,特殊在我们可以通过send()方法向生成器中传入数据,而迭代器只能将数据输出
  • 协程(coroutine):与线程很相似,不同之处在于多协程是同一个线程来执行的,这样就省去了线程切换的时间,而且不需要多线程的锁机制了,执行效率高很多

可迭代对象,迭代器与生成器的关系

简单来说可以用以下的韦恩图表示:

ii
ii

从设计角度讲,容器是我们最常见最常用的数据结构,它们都是可迭代对象,而另一方面我们也可以在自定义类中实现__iter__()方法将该类的对象变成可迭代的

但这样的迭代有一个缺点,当一次迭代的内容过多时,会占用大量内存;这时迭代器就出现了,其实现了__next__()方法,在每个单次迭代中记录位置,每次返回一个值来进行完整的迭代,实现一种惰性的获取数据的方式;这样就不需要一次性把所有内容加载到内存,而是在需要的时候返回单个结果

生成器是一种特殊的迭代器,可以通过生成器函数和生成器表达式来创建生成器,其自带了__iter__()__next__()方法,通过创建生成器来创建迭代器可以让我们更专注于业务逻辑的实现;此外其还实现了send()方法,可以往生成器函数中传值,赋给yield关键字的左值

生成器中的yield

在一个函数中使用yield关键字,这个函数就变成了生成器函数,看一个经典的输出斐波那契数列实现:

代码语言:javascript
复制
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

for num in fib(1000):
    print(num)

非常优雅地就可以实现斐波那契数列的输出,并且在进行for循环时每次只占用一个数的内存

但使用for循环进行迭代我们无法获取到生成器函数的返回值(生成器结束迭代时会抛出StopIteration异常,但这个异常被for循环捕获并pass了);想要获取返回值我们需要抛弃for循环,自己来捕获异常:

代码语言:javascript
复制
g = fib(6)
while True:
     try:
         x = next(g)
         print('g:', x)
     except StopIteration as e:
         print('Generator return value:', e.value)
         break

协程中的yield

Python对协程的支持是通过generator实现的,在一般的generator使用中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值;但在生成器中我们还可以通过send向生成器函数中传值;到这里我们就可以拿出经典的生产者-消费者模型来看看了:

代码语言:javascript
复制
def consumer():
    res = ''
    while True:
        product = yield res
        if not product:
            return
        print('[CONSUMER] Consuming %s...' % product)
        res = '200 OK'

def produce(c):
    c.send(None)
    product = 0
    while product < 5:
        product += 1
        print('[PRODUCER] Producing %s...' % product)
        r = c.send(product)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务

如果没有协程,我们在写一个并发业务时会遇到以下问题:

  • 使用最常规的同步编程要实现异步并发效果并不理想,或者难度极高
  • 由于GIL锁的存在,多线程的运行需要频繁的加锁解锁,切换线程,这极大地降低了并发性能

而有了协程,我们就可以非常优雅高性能地实现一些高IO的并发任务了

yield from

yield fromPython 3.3中才出现的语法,所以这个特性在Python 2中是没有的,yield from语法可以让我们方便地调用另一个generator

yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器或生成器(不记得关系了的可以往上翻一翻)

应用一:拼接可迭代对象

我们可以用一个使用yield和一个使用yield from的例子来对比看

使用yield

代码语言:javascript
复制
# 字符串
astr = 'ABC'
# 列表
alist = [1,2,3]
# 字典
adict = {"name":"wangbm","age":18}
# 生成器(生成器表达式)
agen = (i for i in range(4,8))

def gen(*args, **kw):
    for item in args:
        for i in item:
            yield i

new_list = gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

使用yield from

代码语言:javascript
复制
# 字符串
astr = 'ABC'
# 列表
alist = [1,2,3]
# 字典
adict = {"name":"wangbm","age":18}
# 生成器
agen = (i for i in range(4,8))

def gen(*args, **kw):
    for item in args:
        yield from item

new_list = gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

由上面两种方式的对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰

应用二:生成器的嵌套

上面的只是yield from很简单的一个应用,它真正的作用并不在此;当 yield from 后面加上一个生成器后,就实现了生成器的嵌套

当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现

如果自己用yield去实现,那只会加大代码的编写难度,降低开发效率,降低代码的可读性。既然Python已经想得这么周到,我们当然要好好利用起来

讲解它之前,首先要知道这个几个概念

  • 预激活:通过next()方法或send(None)方法使生成器第一次停在yield关键字处,状态由GEN_CREATED变为GEN_SUSPENDED
  • 调用方:调用委派生成器的客户端(调用方)代码
  • 委托生成器:包含yield from表达式的生成器函数
  • 子生成器yield from后面加的生成器函数

直接看代码:

代码语言:javascript
复制
from inspect import getgeneratorstate


# 子生成器
def average_gen():
    total, count, average = 0, 0, 0
    while True:
        new_num = yield average
        if not new_num:
            break
        count += 1
        total += new_num
        average = total / count
    # return 意味着当前协程结束
    return total, count, average


# 委托生成器
def proxy_gen():
    # 当前协程结束后,进入新的协程并在new_num = yield average处暂停返回给调用方(yield from对子生成器进行预激活)
    while True:
        sub_coroutine = average_gen()
        print(getgeneratorstate(sub_coroutine))  # GEN_CREATED,还未预激活,yield from会进行预激活,使子生成器状态变为GEN_SUSPENDED
        total, count, average = yield from sub_coroutine  # 这里yield from处理了子生成器return时产生的StopIteration Error
        print('sub_coroutine close, stat: {}; total, count, average: {}, {}, {}'
              .format(getgeneratorstate(sub_coroutine), total, count, average))


# 调用方
def main():
    calc_average = proxy_gen()  # 创建委托生成器
    next(calc_average)  # 预激活委托生成器
    
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)  # 在子生成器中return,触发StopIteration Error结束当前协程
	
    print(calc_average.send(20))
    print(calc_average.send(50))
    print(calc_average.send(70))
    print(calc_average.send(40))
    calc_average.send(None)
    calc_average.close()


if __name__ == '__main__':
    main()

输出:

代码语言:javascript
复制
GEN_CREATED
10.0
15.0
20.0
sub_coroutine close, stat: GEN_CLOSED; total, count, average: 60, 3, 20.0
GEN_CREATED
20.0
35.0
46.666666666666664
sub_coroutine close, stat: GEN_CLOSED; total, count, average: 140, 3, 46.666666666666664
GEN_CREATED

上面是一个实时计算平均值的实现,流程这里就不展开讲了,仔细看的都看得清楚,讲一下值得注意的几个点(结合注释):

  • inspect.getgeneratorstate方法可以获取生成器的状态
  • yield from会对子生成器进行预激活
  • 委托生成器只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截
  • yield from会帮我们处理子生成器的所有异常(不只是最常见的StopIteration

总结

yield关键字在Python中可以说很重要了,很多地方的实现都是使用它,尤其在并发编程中,协程的实现也让我们的开发优雅简洁了不少

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-02-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 需要了解的几个词
  • 可迭代对象,迭代器与生成器的关系
  • 生成器中的yield
  • 协程中的yield
  • yield from
    • 应用一:拼接可迭代对象
      • 应用二:生成器的嵌套
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档