Python3中yield与yield from详解

一、yield

学习协程的第一门课程,是要认识生成器,有了生成器的基础,才能更好地理解协程。

如果你是新手,那么你应该知道迭代器,对生成器应该是比较陌生的吧。没关系,看完这系列文章,你也能从小白成功过渡为Ptyhon高手。

本文主要从以下几个方面来学习yield的知识点:

1:可迭代、迭代器、生成器
2:如何运行/激活生成器
3:生成器的执行状态
4:从生成器过渡到协程:yield

1:可迭代、迭代器、生成器

我们如何区分区分一个对象是否是可迭代、迭代器、还是生成器呢?有一个简单的办法:

from collections.abc import Iterable, Iterator, Generator

isinstance(obj, Iterable)        # 可迭代对象
isinstance(obj, Iterator)        # 迭代器
isinstance(obj, Generator)    # 生成器

Iterable:一般在python中想字符串,list, dict, tuple, set, deque等都是可迭代对象,从表象上看他们都可以使用 for 来循坏迭代,但实际上他们并不是迭代器,也不是生成器。因为一个对象只要实现了__iter__ 方法的,均可称为可迭代对象。

扩展知识:

可迭代对象,是其内部实现了,__iter__ 这个魔术方法。
可以通过,dir()方法来查看是否有__iter__来判断一个变量是否是可迭代的。

Iterator:迭代器,一般对象只要实现了__next__ 与 __iter__ 方法的均可称为生成器对象,因为它可以不用for循序来间断的获取元素值(next(obj)).

迭代器,是在可迭代的基础上实现的。要创建一个迭代器,我们首先,得有一个可迭代对象。
注意:迭代器在元素值迭代结束的时候会抛出 StopIteration 异常,这是必要的。

s = "1234abc" 
iterator = iter(s)
isinstance(iterator , Iterator)  # True

扩展知识:

迭代器,是其内部实现了,__next__、__iter__ 这个魔术方法。(Python3.x)
可以通过,dir()方法来查看是否有__next__来判断一个变量是否是迭代器的。

Generator:生成器,是在迭代器的基础上(可以用for循环,可以使用next()),再实现了yield。

yield 是什么东西呢,它相当于我们函数里的return。在每次next(),或者for遍历的时候,都会yield这里将新的值返回回去,并在这里阻塞,等待下一次的调用。正是由于这个机制,才使用生成器在Python编程中大放异彩。实现节省内存,实现异步编程

实现生成器的方法:
(1): 使用列表生成式

# 使用列表生成式,注意不是[],而是()
L = (x * x for x in range(10))
print(isinstance(L, Generator))  # True

(2): 实现了yield的函数

from inspect import getgeneratorstate

def mygen(n): 
       now = 0
        while now < n:
                r = yield now
                now += 1
         raise StopIteration

StopIteration:在生成器工作过程中,若生成器不满足生成元素的条件,就会抛出异常StopIteration,也应该抛出该异常。

注意:
(1): 一般使用for来循环迭代生成器,在生成器结束是python解释器会在for结束后自动捕获StopIteration异常,让我们的程序没有感知

(2): 使用next(gen), 当next最后一个一个yield后,无论后面yield后面有没有return都会抛出StopIteration;  那么此时如何获取生成器函数的返回值呢?你只需要在最后一次的next(gen),使用try...except StopIteration as e即可, 返回值在e.value中。

    try:
        ret = next(gtw)
    except StopIteration as e: 
        print("GGG:", e.value)        # 函数没有返回值,默认None

send(param): 当生成器使用send(param)是,注意以下部分:

a: gen.send(None),相当于next(next), 因为next就是不带参数,默认是send(None)
b: 在gen.close或者抛出StopIteration 之前使用gen.send(100) 或 gen.send("abc")
    r = yield now
此时r的值就是send发送的值。

执行流程如下:
(1):  gen = mygen
(2): print(next(gen) )               # 此时执行到r = yield now,在yield now时,print打印的值为0,生成器暂停并阻塞在yield处, now + 1 该处代码不会执行,因为暂停并阻塞了
(3): print(gen.send(100))       # 此时r = yield now,会先接收到send的参数值,r就是参数的值,程序将会恢复执行yiled后面的代码,直到再次遇到下一个yield ,  此时print打印的值为1,程序再次会暂停并阻塞。

注意:send在上一次yield暂停阻塞处,yield会先接收send的参数值,然后恢复执行后面的程序,直到下一个yield

可迭代象和迭代器,是将所有的值都生成存放在内存中,而生成器则是需要元素才临时生成,节省时间,节省空间。

2:如何运行/激活生成器

由于生成器并不是一次生成所有元素,而是一次一次的执行返回,那么如何刺激生成器执行(或者说激活)呢?激活主要有两个方法:

a: 使用next()        # 相当于gen.send(None) , 第一次启动、激活只能是send(None) , send不能是其他函数
b: 使用generator.send(None)

3: 生成器的执行状态

from inspect import getgeneratorstate, isgeneratorfunction

使用inspect.getgeneratorstate就能判断生成器的状态,一般在其生命周期中,会有如下四个状态:

GEN_CREATED # 等待开始执行
GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)GEN_SUSPENDED # 在yield表达式处暂停
GEN_CLOSED # 执行结束

>>>  gen = mygen(2)
>>> print("1:", getgeneratorstate(gen))        # GEN_CREATED
>>> print(next(gen))   # print(gen.send(None))
>>> print("2:", getgeneratorstate(gen))        # GEN_SUSPENDED
>>> gen.close()
>>> print("3:", getgeneratorstate(gen))        # GEN_CLOSED

4: 从生成器过渡到协程:yield

通过上面的介绍,我们知道生成器为我们引入了暂停函数执行(yield)的功能。当有了暂停的功能之后,人们就想能不能在生成器暂停的时候向其发送一点东西(其实上面也有提及:send(None))。这种向暂停的生成器发送信息的功能通过 PEP 342 进入 Python 2.5 中,并催生了 Python 中协程的诞生。

注意从本质上而言,协程并不属于语言中的概念,而是编程模型上的概念。

协程和线程,有相似点,多个协程之间和线程一样,只会交叉串行执行;也有不同点,线程之间要频繁进行切换,加锁,解锁,从复杂度和效率来看,和协程相比,这确是一个痛点。协程通过使用 yield 暂停生成器,可以将程序的执行流程交给其他的子程序,从而实现不同子程序的之间的交替执行。

def jumping_range(N):
        index = 0 while index < N:
                # 通过send()发送的信息将赋值给
                jump jump = yield index
                if jump is None:
                    jump = 1
                index += jump
if __name__ == '__main__':
itr = jumping_range(5)
print(next(itr))            # 0
print(itr.send(2))        # 2
print(next(itr))            # 3
print(itr.send(-1))       # 2

这里解释下为什么这么输出。

重点是jump = yield index这个语句。

分成两部分:

yield index 是将index return给外部调用程序。

jump = yield 可以接收外部程序通过send()发送的信息,并赋值给jump

以上这些,都是讲协程并发的基础必备知识请一定要亲自去实践并理解它,不然后面的内容,将会变得枯燥无味,晦涩难懂。

二、yield from

yield from 所在的函数被称为委托生成器,它主要为调用方子生成器提供一个双向通道;那么下面我们你主要从以下方面来讲解yield from的相关知识:

1: 为什么要使用协程
2: yield from的用法详解
3: 为什么要使用yield from

1: 为什么要使用协程

在使用yield from之前,请读者把上面的yield的知识好好复习巩固一下。

总的来说asyncio比线程优越的地方就是:协程不像线程那样需要频繁进行上下文切换、加锁、解锁,这些过程,所以协程之间切换的时间开销将大幅减小,效率上将大幅提高。对于爬虫、读写文件、读磁盘等这种非常耗时的IO来说更是如此

def  spider_xx(url):
        html = get_html(url)
        ......
        data = parse_html(html)

我们都知道,get_html()等待返回网页是非常耗IO的,一个网页还好,如果我们爬取的网页数据极其庞大,这个等待时间就非常惊人,是极大的浪费。

聪明的程序员,当然会想如果能在get_html()这里暂停一下,不用傻乎乎地去等待网页返回,而是去做别的事。等过段时间再回过头来到刚刚暂停的地方,接收返回的html内容,然后还可以接下去解析parse_html(html)。

利用常规的方法,几乎是没办法实现如上我们想要的效果的。所以Python想得很周到,从语言本身给我们实现了这样的功能,这就是yield语法。可以实现在某一函数中暂停的效果。

试着思考一下,假如没有协程,我们要写一个并发程序。可能有以下问题

1)使用最常规的同步编程要实现异步并发效果并不理想,或者难度极高。

2)由于GIL锁的存在,多线程的运行需要频繁的加锁解锁,切换线程,这极大地降低了并发性能;

而协程的出现,刚好可以解决以上的问题。它的特点有

协程是在单线程里实现任务的切换的

利用同步的方式去实现异步

不再需要锁,提高了并发性能

2:yield from的用法

yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。

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

def gen_from(*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]

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

当然上面只是小case, yield from的应用远不仅仅如此。当 yield from 后面加上一个生成器后,就实现了生成的嵌套。

当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现,讲解它之前,首先要知道这个几个概念:

1、调用方:            调用委托生成器的客户端(调用方)代码
2、委托生成器:    包含yield from表达式的生成器函数
3、子生成器:         yield from 后面加的生成器函数

委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。

所谓的双向通道是什么意思呢?调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。

你可能会经常看到有些代码,还可以在yield from前面看到可以赋值。这是什么用法?

你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。

from collections import namedtuple
Result = namedtuple('Result', 'count average')

def get_average():
        """ 子生成器 """
         total = 0.0
        count = 0
        average = None
        while True:
                # send 发送值给yield接收, yield 后面可以没有参数;
                # 有参数时 yield average 是为了让调用方迭代获取a值,和 term 没有关系
                term = yield average
                if term is None:
                        break total += term
                        count += 1
                        average = total / count
        return Result(count, average)

def delegate_gen(results, key):
        """ 委托生成器 """
        while True:
                # 只有当生成器 get_average()结束,才会返回结果给results赋值
                # 无 while True 抛 StopIteration print("grouper end")
                results[key] = yield from get_average() 
                # return results      # 有无 while True 都会抛 StopIteration

def call_main(data):
        """ 调用方 """
        results = {}
        for key, values in data.items():
                delegation = delegate_gen(results, key)
                next(delegation) # 启动/激活子生成器,第一次运行到 yield 阻塞暂停
                for value in values:
                        delegation.send(value)
                delegation.send(None) # 结束子生成器(return 了)
        print(results)

代码里面有几个很重要的点,作如下讲解:

1:启动/激活子生成器,next(delegation) 与 delegation.send(None), send参数只能是None
2:yield from 对【调用方】与【子生成器】起到双向通道的作用
3:子生成器结束时,子生成器的返回值为默认值或是其他,都会抛出 StopIteration 异常,但是yield from会自动处理子生成器的该异常,那么ret = yield from delegate_gen(...) 中, ret就是子生成器gen()的返回值, 等价于:
                try:
                        delegation.send(None)
                except StopIteration as e:
                       ret = e.value
4: 关于委托生成器抛出 StopIteration 异常的说明:
        (1):yield from 【在】while True 里,当子生成器结束后,并接收到子生成器的返回值后,委托生成器【不会】再次抛出 StopIteration, 代码如下:
                 while True:
                         yield from get_average() 
        (2): 如果yield from 【不在】while True 里,当子生成器结束后,并接收到子生成器的返回值后, 委托生成器【会】再次抛出  StopIteration, 代码如下:
                yield from get_average() 
        (3): 只要yield from 【不在】while True 里,当子生成器结束后,并接收到子生成器的返回值后, 无论委托生成器函数有无return(无return, 默认None)都【会】抛出  StopIteration

关于 yield from 的功能给出了一段伪代码,如下所示:

#一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象"""
 _i = iter(EXPR)
 try:
         _y = next(_i)
except StopIteration as _e:
         _r = _e.value
 else:
        while 1:
                try:
                        _s = yield _y
                except GeneratorExit as _e:
                        try:
                                _m = _i.close
                        except AttributeError:
                                pass
                        else:
                                _m()
                        raise _e
                except BaseException as _e:
                        _x = sys.exc_info()
                        try:
                                _m = _i.throw
                        except AttributeError:
                                raise _e
                        else:
                                try:
                                        _y = _m(*_x)
                                except StopIteration as _e:
                                        _r = _e.value
                                        break
                else:
                        try:
                                if _s is None:
                                        _y = next(_i)
                                else: _y = _i.send(_s)
                        except StopIteration as _e:
                                _r = _e.value break
RESULT = _r

以上的代码,稍微有点复杂,有兴趣的同学可以结合以下说明去研究看看。

1: 迭代器(即可指子生成器)产生的值直接返还给调用者
2: 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
3: 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
4: 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
5: 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
6: 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
7: 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。

你可能感兴趣的:(Python3中yield与yield from详解)