摘要:记录研究python协程过程中生成器和coroutine的使用

生成器

可迭代对象

判断:Iterable类与可迭代对象对应,可以使用isinstance判断一个对象是否是可迭代对象。

>>> from collections.abc import Iterable
>>> print(isinstance(123, Iterable))
False
>>> print(isinstance('abc', Iterable))
True
>>> print(isinstance([], Iterable))
True
>>> print(isinstance({}, Iterable))
True
>>> print(isinstance((), Iterable))
True

定义:只要一个对象定义了__iter__()方法,那么它就是可迭代对象。

from collections.abc import Iterable
class A():
    def __iter__(self):
        pass
print('A()是可迭代对象吗:',isinstance(A(),Iterable))

迭代器

判断:Iterator与迭代器对应,可以使用isinstance判断一个对象是否是迭代器。

from collections.abc import Iterable
from collections.abc import Iterator
class B():
    def __iter__(self):
        pass
    def __next__(self):
        pass
print('B()是可迭代对象吗:', isinstance(B(), Iterable))
print('B()是迭代器吗:', isinstance(B(), Iterator))

输出:

B()是可迭代对象吗: True
B()是迭代器吗: True

定义:如果一个对象同时实现了__iter__()和__next()__()方法,那么它就是迭代器。

可见,迭代器一定是可迭代对象,但可迭代对象不一定是迭代器。

关于 可迭代对象迭代器详细说明参考 为什么for循环可以遍历list:Python中迭代器与生成器 - 奥辰 - 博客园

生成器

定义:如果一个函数体内部使用yield关键字,这个函数就称为生成器函数,生成器函数调用时产生的对象就是生成器。生成器是一个特殊的迭代器,在调用该生成器函数时,Python会自动在其内部添加__iter__()方法和__next__()方法。把生成器传给 next() 函数时, 生成器函数会向前继续执行, 执行到函数定义体中的下一个 yield 语句时, 返回产出的值, 并在函数定义体的当前位置暂停, 下一次通过next()方法执行生成器时,又从上一次暂停位置继续向下……,最终, 函数内的所有yield都执行完,如果继续通过yield调用生成器, 则会抛出StopIteration异常——这一点与迭代器协议一致。

>>> import inspect
>>> from collections.abc import Iterable
>>> from collections.abc import Iterator
>>> def gen():
...     print('第1次执行')
...     yield 1
...     print('第2次执行')
...     yield 2
...     print('第3次执行')
...     yield 3
...
>>> inspect.isgeneratorfunction(gen) # 判断g是否是生成器函数
True
>>> g = gen()
>>> isinstance(g, Iterable) # 判断g是否是可迭代对象
True
>>> isinstance(g, Iterator) # 判断g是否是迭代器
True
>>>inspect.isgenerator(g) # 判断g是否是生成器对象
True
>>> print(g)
<generator object gen at 0x000001EC6F98A348>
>>> next(g)
第1次执行
1
>>> next(g)
第2次执行
2
>>> next(g)
第3次执行
3
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

注意这个StopIteration异常,可以用来获取生成的器的返回值。如果没有return,则默认生成器执行完毕时返回StopIteration

def gen():
    print('第1次执行')
    yield 1
    print('第2次执行')
    yield 2
    print('第3次执行')
    yield 3
    return "111"

a = gen()
try:
    next(a)
    next(a)
    next(a)
    next(a)
except StopIteration as exc:
    print(exc.value)
    '''运行结果为:
    第1次执行
    第2次执行
    第3次执行
    111
    '''

生成器的使用

  1. 使用close关闭生成器

    def my_generator():
        yield 1
        yield 2
        yield 3
        yield 4
    
    
    g = my_generator()
    print(next(g))
    print(next(g))
    g.close()
    print(next(g))  # 在此处会显示错误
    # print(next(g))
    '''运行结果为:
    1
    2
    显示StopIteration
    '''
  2. send和next方法
    send方法:

    def my_generator():
        value1 = yield "123"
        print("我是send函数发送的值1", value1)
        value2 = yield "789"
        print("我是send函数发送的值2", value2)
        yield "101112"
    
    
    g = my_generator()
    print("-"*20)
    print(g.send(None))  # 启动生成器必须用send(None)
    print("-"*20)
    print(g.send("值1"))
    print("-"*20)
    print(g.send("值2"))
    print("-"*20)
    print(g.send(None)) # 生成器结束,抛出StopIteration的异常
    
    '''运行结果为:
    --------------------
    123
    --------------------
    我是send函数发送的值1 值1
    789
    --------------------
    我是send函数发送的值2 值2
    101112
    --------------------
    '''

    解释启动生成器g必须要用send(None),不能使用send("值1")这种形式。
    第一步:
    g.send(None)激活生成器之后,程序运行到 value1 = yield "123",抛出"123"后,生成器暂停运行,在控制台打印出"123"。
    第二步:
    g.send("值1")重新激活生成器并且给 value1 赋值为 "值1",然后运行到 value2 = yield "789" ,抛出"789"后,生成器暂停运行,在控制台打印出"789"。
    第三步:
    g.send("值2")重新激活生成器并且给 value2 赋值为 "值2",然后运行到yield "101112",抛出"101112"后,生成器暂停运行,在控制台打印出"101112"。
    第四步:
    g.send(None),重新激活生成器,但是因为后面没有yield了,生成器执行完毕,抛出StopIteration异常。

    next方法
    next方法和send方法差不多,除了不能向生成器传递值。

    def my_generator():
        value1 = yield "123"
        print("我是send函数发送的值1", value1)
        value2 = yield "789"
        print("我是send函数发送的值2", value2)
        yield "101112"
    
    g = my_generator()
    print("-"*20)
    print(next(g))
    print("-"*20)
    print(next(g))
    print("-"*20)
    print(next(g))
    print("-"*20)
    print(next(g))
    print("-" * 20)
    
    '''运行结果为:
    --------------------
    123
    --------------------
    我是send函数发送的值1 None
    789
    --------------------
    我是send函数发送的值2 None
    101112
    --------------------
    '''
  3. throw方法
    通过生成器throw方法抛出异常后,后面的所有yield就会被废除。

    def my_generator():
        yield 'a'
        yield 'b'
        yield 'c'
    g = my_generator()
    print(next(g))
    print(next(g))
    print('-------------------------')
    try:
        print(g.throw(StopIteration))
    except StopIteration as exc:
        print("异常值", exc.value)
    print(next(g))
    

    可以看到 yield 'c' 会被废除。
    对比下面的:

    def my_generator():
        while True:
            try:
                yield 'a'
                yield 'b'
                yield 'c'
                yield 'd'
                yield 'e'
            except ValueError:
                print('触发“ValueError"了')
            except TypeError:
                print('触发“TypeError"了')
    
    
    g = my_generator()
    print(next(g))
    print(next(g))
    print('-------------------------')
    print(g.throw(ValueError))
    print('-------------------------')
    print(next(g))
    print(next(g))
    print('-------------------------')
    print(g.throw(TypeError))
    print('-------------------------')
    print(next(g))
    

    解释如下:
    出现这样的结果是不是很意外?它和上面的那个例子只有一个while只差,为什么结果差这么多,解释如下:

    首先print(next(g))两次:会输出a、b,并停留在c之前。

    然后由于执行了g.throw(ValueError),所以会跳过所有后续的try语句,也就是说yield 'c'、yield 'd'、yield 'e'不会被执行,然后进入到except语句,打印出 触发“ValueError"了。然后再次进入到while语句部分,消耗一个yield,此时因为是重新进入的while,小号的依然是第一个yield 'a',所以会输出a。实际上这里的a也就是g.throw()的返回值,因为它返回的是下一个迭代的数;

    然后在print(next(g))两次,会执行yield b’、yield 'c’语句,打印出b、c,并停留在执行完该语句后的位置,即yield 'd'之前。

    然后再g.throw(TypeError):会跳出try语句,从而后面的d,e不会被执行,下次重新进入while,依然打印出a。

    最后,执行了一次print(next(g)),打印出b。

    更多参考:python协程系列(一)——生成器generator以及yield表达式详解_MIss-Y的博客-CSDN博客

yield from

yield from主要有下面三个作用:
第一个作用:针对yield无法获取生成器return的返回值。
如果要获取一个协程的return值,必须捕获StopIteration异常,然后从StopIteration异常中获取值

def my_generator():
    for i in range(5):
        if i==2:
            return '我被迫中断了'
        else:
            yield i
 
def main(generator):
    try:
        print(next(generator))   #每次迭代一个值,则会显式出发StopIteration
        print(next(generator))
        print(next(generator))
        print(next(generator))
        print(next(generator))
    except StopIteration as exc:
        print(exc.value)     #获取返回的值
 
g=my_generator()
main(g)
'''运行结果为:
0
1
我被迫中断了
'''

如果改用yield from 则可以使用

def my_generator():
    for i in range(5):
        if i==2:
            return '我被迫中断了'
        else:
            yield i
 
def wrap_my_generator(generator):  #定义一个包装“生成器”的生成器,它的本质还是生成器
    result=yield from generator    #自动触发StopIteration异常,并且将return的返回值赋值给yield from表达式的结果,即result
    print(result)
 
def main(generator):
    for j in generator:
        print(j)
 
g=my_generator()
wrap_g=wrap_my_generator(g)
main(wrap_g)  #调用
'''运行结果为:
0
1
我被迫中断了
'''

第二个作用:减少生成器的嵌套
(感觉是方便了一点,但是感觉这个作用应该也不是主要作用)

def gen_one():
    subgen = range(10)
    yield from subgen

def gen_two(): # gen_one和 gen_two效果一样
    subgen = range(10)
    for item in subgen:
        yield item

第三个作用:在子生成器和原生成器的调用者之间打开双向通道,两者可以直接通信。

def gen():
    yield from subgen()
def subgen():
    while True:
        x = yield
        yield x+1
def main():
    g = gen()
    next(g)                # 驱动生成器g开始执行到第一个 yield
    retval = g.send(1)     # 看似向生成器 gen() 发送数据
    print(retval)          # 返回2
    g.throw(StopIteration) # 看似向gen()抛入异常

通过上述代码清晰地理解了yield from的双向通道功能。关键字yield from在gen()内部为subgen()和main()开辟了通信通道。main()里可以直接将数据1发送给subgen(),subgen()也可以将计算后的数据2返回到main()里,main()里也可以直接向subgen()抛入异常以终止subgen()。

对于这三个作用的详细说明参考
python协程系列(三)——yield from原理详解_MIss-Y的博客-CSDN博客
Python3 异步编程详解(史上最全篇)_木风卜雨的博客-CSDN博客_python3 异步编程

coroutine

旧式协程:Python3.4开始,新增了asyncio相关的API,语法使用@asyncio.coroutine和yield from实现协程
新式协程:Python3.5中引入async/await语法

协程的必要条件

新式协程中,只有同时具有async和await两个条件,才是协程函数。
下面的例子 test1不是协程对象,test2是协程对象
例子:

import asyncio

async def test1(n):
    i =0
    while i < n:
        yield i
        i = i+1

async def test2(n):
    await asyncio.sleep(5)

type1 = test1(1)
type2 = test2(1)

print(type(type1))
print(type(type2))

'''
运行结果:
<class 'async_generator'>
<class 'coroutine'>
'''

判断是否是协程

可以使用asyncio.iscoroutinefunction和asyncio.iscoroutine判断是否是协程函数和协程对象。

import asyncio
@asyncio.coroutine
def test_yield_from(n):
    print(n)
# 是否是协程函数
print(asyncio.iscoroutinefunction(test_yield_from))
# 是否是协程对象
print(asyncio.iscoroutine(test_yield_from(3)))

@asyncio.coroutine与async源码分析

@asyncio.coroutine的源码

import functools
import types
import inspect
def coroutine(func):
    """Decorator to mark coroutines.

    If the coroutine is not yielded from before it is destroyed,
    an error message is logged.
    """
    # 如果是async关键字修饰的新式协程,直接返回
    if inspect.iscoroutinefunction(func):
        # In Python 3.5 that's all we need to do for coroutines
        # defined with "async def".
        return func
    # 检查是不是生成器函数,如果是直接返回
    if inspect.isgeneratorfunction(func):
        coro = func
    # 如果不是生成器函数,就封装一个生成器函数
    else:
        @functools.wraps(func)
        def coro(*args, **kw):
            res = func(*args, **kw)
            if (base_futures.isfuture(res) or inspect.isgenerator(res) or
                    isinstance(res, CoroWrapper)):
                res = yield from res
            else:
                # If 'res' is an awaitable, run it.
                try:
                    await_meth = res.__await__
                except AttributeError:
                    pass
                else:
                    # 判断生成器对象是不是await的
                    if isinstance(res, collections.abc.Awaitable):
                        res = yield from await_meth()
            return res
    wrapper = coro
    # _is_coroutine是一个object对象,_is_coroutine = object()
    # 使用@asyncio.coroutine创建的协程函数
    # 不会被inspect.iscoroutinefunction认为是协程函数。
    # asyncio.iscoroutinefunction判断函数是否是一个协程时
    # 会通过_is_coroutine属性判断
    wrapper._is_coroutine = _is_coroutine  # For iscoroutinefunction(). 
    return wrapper

async的源码

我懒得去翻源码了直接略过....

async与@asyncio.coroutine的区别

import asyncio
import inspect

@asyncio.coroutine
def test1(n):
    print(n)

async def test2(n):
    print(n)


旧式写法 = test1(1)
新式写法 = test2(2)
print("使用@asyncio.coroutine关键字的协程")
print("使用inspect判断旧式写法是否是协程函数", inspect.iscoroutinefunction(test1))
print("使用inspect判断旧式写法是否是协程对象", inspect.iscoroutine(旧式写法))
print("使用asyncio判断旧式写法是否是协程函数", asyncio.iscoroutinefunction(test1))
print("使用asyncio判断旧式写法是否是协程对象", asyncio.iscoroutine(旧式写法))
print("-"*20)
print("使用async关键字的协程")
print("使用inspect判断新式写法是否是协程函数", inspect.iscoroutinefunction(test2))
print("使用inspect判断新式写法是否是协程对象", inspect.iscoroutine(新式写法))

'''
运行结果:
使用@asyncio.coroutine关键字的协程
使用inspect判断旧式写法是否是协程函数 False
使用inspect判断旧式写法是否是协程对象 False
使用asyncio判断旧式写法是否是协程函数 True
使用asyncio判断旧式写法是否是协程对象 True
--------------------
使用async关键字的协程
使用inspect判断新式写法是否是协程函数 True
使用inspect判断新式写法是否是协程对象 True
'''

inspect.iscoroutinefunction和asyncio.iscoroutinefunction的区别

使用@asyncio.coroutine创建的协程函数,不会被inspect.iscoroutinefunction认为是协程函数。asyncio.iscoroutinefunction判断函数是否是一个协程时,会通过_is_coroutine属性判断。

  • inspect.iscoroutinefunction源码

    def iscoroutinefunction(object):
        """Return true if the object is a coroutine function.
    
        Coroutine functions are defined with "async def" syntax.
        """
        return bool((isfunction(object) or ismethod(object)) and
                    object.__code__.co_flags & CO_COROUTINE)
  • asyncio.iscoroutinefunction源码

    def iscoroutinefunction(func):
        """Return True if func is a decorated coroutine function."""
        return (inspect.iscoroutinefunction(func) or
                getattr(func, '_is_coroutine', None) is _is_coroutine)
    

协程的状态

>>> def simple_coroutine():
    print("What's your name?")
    name = yield
    print("Hello", name, ", where are you from?")
    nation = yield
    print("OK, I see")
>>> from inspect import getgeneratorstate
>>> coro = simple_coroutine()
>>> getgeneratorstate(coro)
'GEN_CREATED'
>>> next(coro)
What's your name?
>>> getgeneratorstate(coro)
'GEN_SUSPENDED'
>>> coro.send('Li Yan')
Hello Li Yan , where are you from?
>>> getgeneratorstate(coro)
'GEN_SUSPENDED'
>>> coro.send('China')
OK, I see
Traceback (most recent call last):
  File "<pyshell#92>", line 1, in <module>
    coro.send('China')
StopIteration
>>> getgeneratorstate(coro)
'GEN_CLOSED'

除了'GEN_CREATED','GEN_SUSPENDED','GEN_CLOSED',还有'GEN_RUNNING',不过我们很少观察到此状态。

  • GEN_CREATED:等待执行,即还没有进入协程
  • GEN_RUNNING:解释器执行(这个只有在使用多线程时才能查看到他的状态,而协程是单线程的)
  • GEN_SUSPENDED:在yield表达式处暂停(协程在暂停等待的时候的状态)
  • GEN_CLOSED:执行结束(协程执行结束了之后的状态)

通过 coro = simple_coroutine() 我们得到了一个协程(生成器),此时它处于 'GEN_CREATED' 状态,我们还不能向它传递值,通过调用 next(coro) 启动协程,直到运行到 yield 暂停,等待我们传入数据,coro.send('Li Yan') 使 yield 赋值为'Li Yan',name 获得值 'Li Yan',并继续运行直到下一个yield,在我们传入数据 'China' 之后,nation获得值 'China' 并继续运行,直到触发了StopIteration,此时协程处于'GEN_CLOSED'状态。

调试代码备份

下面是研究协程和生成器时的代码备份。

参考

参考自:Python协程初探