专栏首页零基础使用Django2.0.1打造在线教育网站Python3中的生成器进阶(send/close/throw)

Python3中的生成器进阶(send/close/throw)

C10M问题

随着互联网的发展,C10K的并发已经不能满足日常需要,于是产生了新的挑战,即C10M问题:如何利用8核CPU,64G内存,在10gbps的网络上保持1000万的并发连接,这时候协程就产生了。

在前面介绍过协程的目的是,让我们既能拥有回调模式的高性能,又有同步编程方式的写法。目前面临的问题是:1、回调模式编码复杂度高;2、同步编程的并发性不高;3、多线程编程需要线程间通信(目前是lock机制);对于这些问题我们采取的办法就是:1、使用同步的方式去编写异步的代码;2、使用单线程去切换任务,但是这其中会有很大的困难:1、线程是由操作系统切换的。如果自己声明一个线程,操作系统会帮我们切换,现在我们需要在单线程中切换,意味着需要程序员自己去调度任务,此时操作系统不再帮我们完成,但是如果能实现这点,它的好处就是不再需要锁(锁的目的是完成线程间的同步,而此处只有一个线程,是在一个线程内切换任务,因此并发性就很高)当然并发性高的原因不仅仅是没有锁,还有就是线程的代价大,新建一个线程对操作系统的代价很大,而且它的切换过程较慢,如果达到了在单线程中切换,就好比是函数之间的调用,这样就能知道它的代价小的多了吧。(函数之间的调用性能是远远高于线程切换的性能)。2、单线程内切换函数的性能远高于线程切换,并发性更高,这就好比你在内部声明1000个函数的切换比声明1000个线程切换,性能上会高很多,而且操作更容易,因为占用内存更少。

总结一下使用单线程去切换任务的两个困难: 1、线程是由操作系统切换的,在单线程中切换意味着需要程序员自己去调度任务; 2、不需要锁机制,并发性很高,若在单线程内切换函数,其性能远高于线程切换,其并发性更高。

这里面最大的挑战就是线程内切换函数,这里以抓取某个产品页和详情页为例进行说明:

def get_url1(url): # 获取某个url页面上所有商品的子链接url # 通过子链接url来获取商品的详情信息,这个过程需要获取商品信息的源码,该过程消耗IO html = get_html(url) # 上面就是同步的模式 info = parse_info(html) # 解析商品信息,该过程消耗CPU

上面是同步编程的模式,逻辑非常清晰,先获取某个url页面上所有商品的子链接url,然后通过子链接url来获取商品的详情信息,最后解析商品信息。其中获取详情信息的过程是耗IO操作,解析商品信息是耗CPU操作,因此关注的重点是耗IO操作。由于耗IO操作花费时间较长,因此希望程序运行到此时能跳出来,去执行另一个get_url2函数,也就是说当get_url1运行到get_html(url)时就等待,跳出来去运行get_url2函数,但是注意当我们运行get_url2函数时,已经跳出去了,传统函数的执行过程其实是有一个系统栈来记录的,如下图:

当我们开始启动程序,函数A运行到函数B的时候,会去调用函数B; 而函数B运行到函数C的时候,会去调用函数C;而函数C运行到函数3的时候继续往后走,当函数3执行完就会回到函数B的2位置,继续往后执行函数B中3的代码,当函数B中3的代码执行完毕,就回到函数A的2位置,继续执行到函数A的3处,进而完成函数的运行后退出程序。也就是说它A函数代码运行到B处只是运行B函数中的逻辑,而不是直接暂停B函数的运行,转而运行A函数3处的逻辑了,这个是我们需要的,但是它目前还做不到。因此想采用传统编程模式来实现我们的目标几乎是不可能了,因为函数一旦执行了,就会一直等待它的返回。 我们希望的是当get_url1运行到get_html(url)时就暂停执行,转而运行其他的耗CPU的逻辑,运行完后再切回这个代码。要是现在有一个可以暂停的函数,并且在适当的时候恢复该函数的继续执行,有了这个函数就能采用同步的方式进行编码了,这就是协程。

协程

协程,有人说它是有多个入口的函数,也有人说它是可以暂停的函数,且可以向暂停的地方传入值,它这种函数执行不再是依赖于栈。”可以暂停的函数”?生成器可以暂停啊,是的,那么生成器是如何变成协程的呢?往下看你就知道了。

01

生成器进阶

在前面介绍了生成器的基础内容,接下来介绍生成器的高级内容,因为生成器与协程之间其实还是有一些区别的,了解了这些区别以后才能将生成器变为协程。先通过一段代码简单回忆之前的内容:

def generate_func():
    yield "envy1"
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(gen)
    print(next(gen))
    print(next(gen))
    print(next(gen))
    print(next(gen))

//运行结果:
<generator object generate_func at 0x0000018AA53D3F68>
envy1
envy2
Traceback (most recent call last):
envy3
  File "I:/Python3.6/corroutine_test.py", line 29, in <module>
    print(next(gen))
StopIteration: envy4

我们调用generate_func()函数并不会直接执行该函数,而是得到一个生成器对象。然后对生成器对象调用next()函数,generate_func()函数就会执行第一个yield处,于是产出一个值envy1,注意:这时候generate_func()函数就暂停在yield处,直到第二次调用next()函数才会从此处继续运行。 因此可以发现,生成器函数是可以暂停的函数,它在调用方的驱使下(调用方使用next()函数),每次执行到yield处,将yield的值产出给调用方后就暂停自己的执行,直到调用方下一次驱动它执行。 上面结果是意料之中的,生成器实现了迭代协议,因此可以迭代,但由于越界抛出StopIteration。

02

生成器方法--send

上面介绍的只是生成器的基本功能,其实生成器除了产出值外,还能接收值。

def generate_func():
    html = yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    print(html)    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    url = next(gen)    # 将html信息进行修改
    html = "hello envy"
    gen.send(html)

//运行结果:
hello envy

启动生成器有两种:一种是next,另一种是send方法。send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置,也就是说它具有了next的功能,同时还能传递值进入生成器内部,尝试打印一下:

print(gen.send(html))//运行结果:hello envy
envy2

html = yield “http://www.baidu.com"这句话有两个作用:1、可以产出值;2、可以接收值(调用方传递进来的值),也就是说yield在等号左边只能是产出值,在等号右边既能产出值,又能接收值。 细心的你可能会有疑问,开头为什么使用next而不是send,而send本身就具有了next的功能啊,这个问题问的很好,那我们就修改一下代码:

def generate_func():
    html = yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    print(html)    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    url = gen.send("hello world")    print(url)//运行结果:
 url = gen.send("hello world")
TypeError: can't send non-None value to a just-started generator

看到最后错误提示没,说生成器刚刚开始,无法发送一个不为None的值。由此可见,在使用生成器时,第一次要发送一个值为None的变量。是吗,那就将url = gen.send(“hello world”)修改为url = gen.send(None),然后再次运行一下(注意此处输出的肯定是mian函数中的print(url)运行结果,而不是generate_func函数中print(html)运行结果,因为无论是next还是send运行到yield处就会停止):

def generate_func():
    html = yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)if __name__ == "__main__":
    gen = generate_func()    # 启动生成器有两种。一种是next,另一种是send
    url = gen.send(None)
    print(url)

//运行结果:
http://www.baidu.com

程序居然正常运行了呢?那么在使用生成器时,第一次为什么要发送一个值为None的变量呢?那是因为前面说过send方法是将值发送给html = yield “http://www.baidu.com"对象,但是这时候如果生成器在初始化的时候没有启动生成器的话是无法接收信息的,也就是根本运行不到这行代码的,因此你在send一个非None值它就会报错,其实就是生成器还未启动罢了。 在使用send方法发生非None对象时,必须先启动生成器,可以通过next方法,或者是send(None)方式。 当然如果此时函数中只有一个yield,你执行完后再次使用send方法就会报StopIteration错误,这是很明显的错误。

03

生成器方法--close

顾名思义close方法就是用于关闭生成器,但是这里面有坑需要填,先看一段代码:

def generate_func():
    yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    next(gen)

//运行结果:
http://www.baidu.com

Traceback (most recent call last):
    print(next(gen))
StopIteration

第一次启动生成器,并输出第一个yield处信息,然后调用close方法关闭生成器,接着再次调用next方法程序就报错了。这个看似很好理解,但是有坑,为什么呢?先不管是什么坑,按照之前的逻辑这里除了错,那我们就捕获一下呗:

def generate_func():
    yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)

    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()    try:
        print(next(gen))    except Exception:        pass

但是这个并不是我们想要的,因为已经知道关闭生成器后再调用next就会报错,这里的意思就是报错了不去处理,这并不是我们所想要的。其实真正的问题在于第一个yield处,因为代码在执行到该处后就停止运行,因此其实是GeneratorExit:

def generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except GeneratorExit:        pass
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    gen.close()
RuntimeError: generator ignored GeneratorExit

看到没,程序执行到print(next(gen))的时候,其实就停止在yield “http://www.baidu.com"处,因此会输出http://www.baidu.com,然后当你调用gen.close()时,就捕获了GeneratorExit异常,不过好奇的是捕获了异常后,我们没有进行什么操作,按理来说应该回运行main函数中的print(next(gen))从而输出"envy2",事实上你也看见了,直接报错了,没有往下执行,这是为什么呢?那是因为在调用gen.close()的时候,生成器就已经关闭了,因此执行生成器相关的代码逻辑(含有yield关键词的语句)就会报错,其他的语句不会报错:

def generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except GeneratorExit:        pass
    # yield "envy2"
    # yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    print(next(gen))
StopIteration

确实是这样,这里报错原因不再是之前那个,而是由于yield关键词数量与next函数不匹配,其实就是生成器迭代越界罢了。 这里总结一下:当生成器调用close方法后,若其中包含未执行的含yield代码,则程序会在gen.close()处报RuntimeError: generator ignored GeneratorExit错误;若其中不包含未执行的含yield代码,则程序会在next(gen)处报StopIteration错误(前提是继续执行了next(gen))。 现在生成器调用close方法且其中包含未执行的含yield代码,我们又不想让它报错,那该怎么办呢?此时应该处理这个异常:

def generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except GeneratorExit:        raise StopIteration    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    print(next(gen))
StopIteration

发现了么,此时只会在next(gen)处报StopIteration错误;而不会在gen.close()处报RuntimeError: generator ignored GeneratorExit错误,对比一下:

def generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except GeneratorExit:        pass
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    gen.close()
RuntimeError: generator ignored GeneratorExitdef generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except GeneratorExit:        raise StopIteration    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    print(next(gen))
StopIteration

通过对比是不是印象更深刻一些了呢?因此如果自己处理GeneratorExit时,最后是使用raise StopIteration这样就不会在运行时报错。不知道你发现没有,这种其实跟你什么也没做,即最开始那段代码的运行结果完全一致:

def generate_func():
    yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print(next(gen))

//运行结果:
http://www.baidu.com
Traceback (most recent call last):
    print(next(gen))
StopIteration

那如此看来上面讲的似乎没什么用啊,但是大家对于到底在哪里出了错,以及出的什么错更加深刻了。其实这里想表达的是,当生成器调用close方法后,它就关闭了,你不去调用它就不会产生错误,就是这么一个非简单的道理:

def generate_func():
    yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print("hello envy")

//运行结果:
http://www.baidu.com
hello envy

上述代码肯定是没问题的,其实下面也是没有问题的,捕获了异常却什么也没操作:

def generate_func():
    try:        yield "http://www.baidu.com"   # 1、可以产出值;2、可以接收值(调用方传递进来的值)
    except Exception:        pass
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.close()
    print("hello envy")

//运行结果:
http://www.baidu.com
hello envy

那么为什么最简单的不会出问题,而加了异常捕获却不处理,其实跟它也一样,这里为什么还要拿出了呢?这里其实是想告诉你Exception其实并不是异常的基类,而BaseException才是:

class Exception(BaseException):
    """ Common base class for all non-exit exceptions. """

是不是没想到呢?前面的GeneratorExit其实是继承了BaseException,而没有继承Exception:

class GeneratorExit(BaseException):
    """ Request that a generator exit. """

看到这里你是不是豁然开朗,怪不得异常捕获不去处理和没加捕获结果一样,其实我么异常捕获错了,应该是BaseException或者GeneratorExit,此时捕获了不去处理就会报错:

Traceback (most recent call last):    gen.close()RuntimeError: generator ignored GeneratorExit

这才是正确的异常处理。好了close方法就介绍到这里,下面介绍throw方法。

生成器方法—throw throw就是往里面扔一个异常,看一段代码:

def generate_func():    yield "http://www.baidu.com"
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()    print(next(gen))
    gen.throw(BaseException, "下载失败 ")//运行结果:http://www.baidu.comTraceback (most recent call last):
    gen.throw(BaseException, "下载失败 ")    yield "http://www.baidu.com"BaseException: 下载失败

发现没有这个异常居然是在yield “http://www.baidu.com"处而不在yield “envy2”处,尽管我们将其值yield出来了,但是还是报错,这个和前面介绍的close是不一样的,这个异常需要处理:

def generate_func():
    try:        yield "http://www.baidu.com"
    except BaseException:   # 扔什么异常就处理什么异常
        pass
    yield "envy2"
    yield "envy3"
    return "envy4"if __name__ == "__main__":
    gen = generate_func()
    print(next(gen))
    gen.throw(BaseException, "下载失败 ")

//运行结果:
http://www.baidu.com

如果我们在后面再次调用print(next(gen))呢?会不会报错呢?答案是:”envy3”,这里就很奇怪了,不报错可以理解,毕竟自己扔的异常已经处理了,但是输出为什么不是”envy2”而是”envy3”呢?是这样的,其实在yield “http://www.baidu.com"处发生了问题,此时已经跳转到yield “envy2”了,因此再次调用print(next(gen))会从yield “envy3”处运行代码。此时如果你又在后面添加了gen.throw(BaseException, “下载失败 “)函数肯定会报之前的错误,因为你又扔了一个异常但是又没处理。

那么到现在生成器通过send、close、throw方法已经初步达到我们对协程的要求,可以产值、传值、扔异常、关闭等操作,但是这还不够,还需要另一个方法yield from。

本文分享自微信公众号 - 啃饼思录(kbthinking),作者:i思录

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 测开之函数进阶· 第1篇《递归函数》

    第一个print(next(g))打印的 0,就是生成器生成的元素。第二个print(next(g))打印的 1 也是生成器生成的元素,None 是print(...

    清菡
  • 生成器进化到协程 Part 1

    这篇文章大部分来自 David Beazley 在 PyCon 2014 的 PPT 《Generators: The Final Frontier》。这个PP...

    py3study
  • 学习python协程前你必须了解的知识

    讲到迭代器,就需要区别几个概念:iterable, iterator, itertion, 看着都差不多,其实不然。下面区分一下。

    星星在线
  • 从yield 到yield from再到python协程

    yield 是在:PEP 255 -- Simple Generators 这个pep引入的

    coders
  • python的协程

    yield指令有两个功能:yield item用于产出一个值,反馈给next()的调用方。

    哒呵呵
  • [译]PEP 342--增强型生成器:协程

    PEP原文 : https://www.python.org/dev/peps/pep-0342/

    Python猫
  • python协程1:yield的使用

    python2.5 中,yield关键字可以在表达式中使用,而且生成器API中增加了 .send(value)方法。生成器可以使用.send(...)方法发送数...

    goodspeed
  • kafka学习二 -发送消息

    从源码中我们发现在Sender的run方法中,并没有涉及到append追加操作。因此可以看到源码中,如果消息收集器中的消息收集结果为空或者新的消息批次已经创建好...

    路行的亚洲
  • RocketMQ学习5

    进行消息发送的过程首先会准备好路由信息,最终是由netty完成的,也即使用nettyRemotingClient来实现的。

    路行的亚洲
  • Tars-C++ 揭秘篇:文件描述符处理“套路”

    本章总结Tars中对文件描述符进行操作时的一些“套路”的做法,偏重异常时候的处理。这些处理方式在任何RPC框架中都是值得考虑的

    路小饭
  • Python进阶——如何正确使用yield?

    在 Python 开发中,yield 关键字的使用其实较为频繁,例如大集合的生成,简化代码结构、协程与并发都会用到它。

    _Kaito
  • Netty在Dubbo中的使用过程源码分析

    最近项目中使用了netty服务,空余时间差了下dubbo中是如何使用netty做底层服务的,找了相关资料记录一下:

    小勇DW3
  • kafka学习三-broker的入口

    前面我们通过学习scala知道通常如果想运行scala程序,必然会有一个入口,而这个入口可以通过kafka的启动脚本kafka-server-start.sh可...

    路行的亚洲
  • [译]PEP 380--子生成器的语法

    导语: PEP(Python增强提案)几乎是 Python 社区中最重要的文档,它们提供了公告信息、指导流程、新功能的设计及使用说明等内容。对于学习者来说,PE...

    Python猫
  • GoTTY - 终端工具变为 Web 应用

    访问 http://127.0.0.1:8080 即可在线体验 Python3 环境。

    子润先生
  • 谈谈 Python 的生成器

    第一次看到Python代码中出现yield关键字时,一脸懵逼,完全理解不了这个。网上查下解释,函数中出现了yield关键字,则调用该函数时会返回一个生成器。那到...

    顶级程序员
  • Java异常体系中的秘密

    相信大家每天都在使用Java异常机制,也相信大家对try-catch-finally执行流程烂熟于胸。本文将介绍Java异常机制的一些细节问题,这些问题虽然很...

    大闲人柴毛毛
  • 聊聊flink的SocketClientSink

    flink-streaming-java_2.11-1.7.0-sources.jar!/org/apache/flink/streaming/api/data...

    codecraft
  • 聊聊flink的SocketClientSink

    flink-streaming-java_2.11-1.7.0-sources.jar!/org/apache/flink/streaming/api/data...

    codecraft

扫码关注云+社区

领取腾讯云代金券