[Python] 理解yield关键字、生成器函数和协程

写作背景

主旨:

  1. 介绍yield关键字的用法、生成器和生成器函数
  2. 什么是协程、生成器函数和协程的区别
  3. 从yield到yield from
  4. future处理并发
  5. asyncio处理并发
  6. 异步爬虫实战

很多面试官,上来就是喂你一道协程。很多文章,上来就是教你用asyncio。太难了呀。所以我想写一写自己学到的东西,把有关协程和异步编程的一整个系列写下来。可能会有不对的地方,希望各位看官多多指教。

一、从生成器函数到协程

协程是异步编程的基础,为了理解异步编程,我们就得先搞明白什么是协程。而协程和生成器函数息息相关,因为协程也是一种生成器函数。所以我们再看看生成器函数。

1. yield、生成器和生成器函数

回顾一个例子,demo1()里面使用了yield,是一个生成器函数,返回一个生成器。我们可以使用next()方法来迭代生成器,每次迭代next()获取到的就是yield产生的值,也就是yield右边的值

# yield可以理解为暂停按钮,每次执行到yield,保存断点,同时yield还会返回值给调用方
def demo1(value=None):
    print('start')
    yield 1
    yield value
    yield 2
    print('end')

g = demo1("Value")	# 生成器函数也是函数,可以接收传参
print(type(g))   # g是一个生成器
print(next(g))   # 执行yield 1,暂停
print(next(g))   # 执行yield value,暂停
print(next(g))   # 执行yield 2,暂停
print(next(g))   # 找不到yield了,raise StopIteration
<class 'generator'>
start
1
Value
2
end
Traceback (most recent call last):
  File "D:/Desktop/Projects/效率编程/协程/test.py", line 14, in <module>
    print(next(g))   # 找不到yield了,raise StopIteration
StopIteration

总共三次yield,我们是调用方,使用next(g)收到了三个yield返回值,当我们尝试调用第四次时,demo1函数产生了一个StopIteration告诉我们找不到yield了,raise StopIteration。

2. next()和send(None)

为了将协程,我们先讲讲next()和send()的因缘,next() == send(None)。但是send()是为了传值给生成器而是用的,这个后面会解决

>>> def demo2(value=None):
...     print('start')
...     a = yield 1
...     print("demo1 Received: ", a)
...     b = yield value
...     print("demo1 Received: ", b)
...
>>> g = demo2("Value")
>>> next(g)
start
1
>>> g = demo2("Value")
>>> g.send(None)
start
1
>>>

为什么说next() == send(None)?我们看看底层代码!不感兴趣的可以跳过。

3. send()和next()的C语言实现

Python底层是由C语言实现的,让我们摘出next()和send()来观察看看!

static PyObject *
gen_iternext(PyGenObject *gen)
{
    return gen_send_ex(gen, NULL, 0);  # 传入NULL
}


static PyObject *
gen_send(PyGenObject *gen, PyObject *arg)
{
    return gen_send_ex(gen, arg, 0);  # 传入可变参数
}

4. send() 给生成器函数传值

我们在上面讲到了yield通过yield item可以返回值给调用方,调用方可以通过next()获取到yield的产出值。其实yield还可以接受调用方传来的值,通过data = yield,生成器函数内就可以接收调用方传来的值。data = yield item 可以产出一个值item给调用方,同时接收调用方(调用方使用send)传来的值,然后暂停执行,作出让步,使调用方继续工作,直到调用方下次继续执行send。

  1. yield item:产出值item给调用方。
  2. data = yield:data接收值,下一次send()时,data才会接收到上一次send()的值。
  3. data = yield item:产出值和接收值,先yield item产出值,下一次send()时,data才会接收到上一次send()的值。所以分为yield item 和 data = yield 两步走。

为了理解产出值和接收值,我们看一个产出值和接收值的例子:

def demo2(value=None):
    print('start')
    a = yield 1
    print("demo1 Received: ", a)
    b = yield value
    print("demo1 Received: ", b)
    c = yield 2
    print("demo1 Received: ", c)
    print('end')

g = demo2("Value")
print(type(g))   # g是一个生成器
print(next(g))   # 执行yield 1,暂停
print(g.send(100))   # 执行yield value,暂停
print(g.send(200))   # 执行yield 2,暂停
print(g.send(300))   # 找不到yield了,raise StopIteration
<class 'generator'>
start
1
demo1 Received:  100
Value
demo1 Received:  200
2
demo1 Received:  300
end
Traceback (most recent call last):
  File "D:/Desktop/Projects/效率编程/协程/test.py", line 17, in <module>
    print(g.send(300))   # 找不到yield了,raise StopIteration
StopIteration

5. 使用Pycharm的DEBUG模式理解data = yield item的执行过程

还是上面的那段代码,在14行打上断点:

def demo2(value=None):
    print('start')
    a = yield 1
    print("demo1 Received: ", a)
    b = yield value
    print("demo1 Received: ", b)
    c = yield 2
    print("demo1  Received: ", c)
    print('end')

g = demo2("Value")
print(type(g))   
print(next(g))   # 在这里打上断点
print(g.send(100))   
print(g.send(200))   
print(g.send(300))  
  1. 在Pycharm中,按Shift+F9,进入Debug模式,代码运行到13行:
    在这里插入图片描述
    我们发现g是一个’generator’。

  2. 接下来按F7就行了。按F7 首次进入demo2函数内,连续按3次F7之后,控制台打印了’start’和1,此时执行到14行:
    在这里插入图片描述

  3. 在运行到第一个send(),也就是第二次进入demo2函数内的时候,a才被赋值100。
    在这里插入图片描述

  4. 在运行到第二个send(),第三次进入demo2函数内的时候,b才被赋值200。c同理会在第四次进入demo2函数才被赋值。
    在这里插入图片描述

二、协程

1. 什么是协程?

什么是协程?这是第一个问题。很多博客和书都在讲协程,但是并没有给出协程定义。也有人把协程叫做“微线程”,下面给出书上的一句话:

协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。 ——《流程的Python》

Python的协程,其实就是包含data = yield的生成器函数,当然也可以是包含data = yield item

我们上面讲的例子,demo2()就是一个协程:

def demo2(value=None):
    print('start')
    a = yield 1
    print("demo1 Received: ", a)
    b = yield value
    print("demo1 Received: ", b)
    c = yield 2
    print("demo1  Received: ", c)
    print('end')

2. 协程的基本操作和四种状态

协程有四种状态,可以使用inspect.getgeneratorstate()确定协程状态:

  • ‘GEN_CREATED’:等待开始执行。
  • ‘GEN_RUNNIGN’:正在执行。
  • ‘GEN_SUSPENDED’:在yield表达式处暂停。
  • ‘GEN_CLOSED’:执行结束。

协程(生成器)有四种跟调用方交互的方法():

  • next():激活协程(生成器)。执行到第一个yield处暂停,将控制权交给调用方,使协程变为‘GEN_CREATED’状态。
  • send():发送数据给协程(生成器),发送的数据会成为yield表达式的值。这一步必须在协程变为‘GEN_CREATED’状态之后使用,也就是必须先使用next()或send(None)激活协程。
  • throw():使协程(生成器)在暂停的yield处抛出指定的异常。
  • close():终止协程(生成器)。使协程(生成器)在暂停的yield处抛出GeneratorExit异常。

我们看一个例子,使用这四种交互方法,并查看生成器的状态:

from inspect import getgeneratorstate	# 导入获取生成器状态的包

class DemoException(Exception):	# 自定义一个异常
    pass

def demo3():
    print('start')
    while True:
        try:
            x = yield
        except DemoException:
            print("demo3 Received DemoException!")
        else:
            print("demo3 Received: ", x)
    # print('end')    # 这一行代码永远不会被执行
  1. next() 或者 send(None) 预激协程:
>>> c = demo3()
>>> next(c)   # 执行第一个yield,暂停
start
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
>>> 
>>> c = demo3()
>>> c.send(None)
start
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
  1. send()
>>> c.send(1)
demo3 Received:  1
>>> c.send(2)
demo3 Received:  2
>>> getgeneratorstate(c)
'GEN_SUSPENDED'
  1. throw()
>>> c.throw(DemoException)	# 调用方丢一个DemoException异常给协程,协程内定义了DemoException的处理方式
demo3 Received DemoException!
>>> getgeneratorstate(c)	
'GEN_SUSPENDED'
>>> 
>>> c.throw(ValueError)		# 调用方丢一个ValueError异常给协程,协程内未定义处理方式
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in demo3
ValueError	# 协程将异常返回给调用方
>>> getgeneratorstate(c)	# 没办法处理异常会使协程状态变为closed
'GEN_CLOSED'
  1. close()
>>> c = demo3()
>>> c.close()
>>> getgeneratorstate(c)
'GEN_CLOSED'

我们不能在协程内捕获GeneratorExit异常,否则协程会抛出一个RuntimeError:

def demo3():
    print('start')
    while True:
        try:
            x = yield
        except GeneratorExit:		# 尝试捕获GeneratorExit异常
            print("demo3 Received GeneratorExit!")
        else:
            print("demo3 Received: ", x)
>>> c = demo3()
>>> next(c)
start
>>> c.send(1)
demo3 Received:  1
>>> c.close()
demo3 Received GeneratorExit!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: generator ignored GeneratorExit

如果想在协程结束时做某些事,可以使用try...finally...包裹协程:

def demo3():
    print('start')
    try:
        while True:
            x = yield
            print("demo3 Received: ", x)
    finally:
        print('end')
>>> c = demo3()
>>> next(c)
start
>>> c.send(1)
demo3 Received:  1
>>> c.close()
end				# 调用close()后,输出了end

3. 个人对yield和协程的理解

3.1 如何理解协程

普通的函数,都是等待接收方调用,调用一次执行(接收参数、执行函数体、返回值)就结束了。如果有一个函数,在一次调用内,既能够接收你传入的值n次,又能返回值m次给你,在执行过程中,可以暂停等待你传值,直到你需要再次启动,是不是感觉有点牛逼?没错,我们前面看到的,Python里yield关键字不就是做这件事的吗?data = yield item 可以产出一个值item给调用方,同时接收调用方(调用方使用send)传来的值,然后暂停执行,作出让步,使调用方继续工作,直到调用方下次继续执行send。

总之,data = yield item 执行过程可以理解为产出值、暂停、接收值上一次send传来的值,依次反复。

3.2 协程FAQ

一问:协程跟生成器是什么关系?

协程是生成器的一种,协程是一种控制流程,四种交互方法本质就是调用方跟生成器交互,因为生成器函数返回的就是生成器,而协程也是由生成器函数返回的。

二问:为什么要先使用next()预激活协程?

说了协程是一种控制流程。假如你雇了一个人给你装修房子,你愿意在你还没允许的情况下这个人就开始装修吗?所以,调用方得先使用next()告诉协程,我要你准备开始跑了,你才能开始。

3.3 区别生成器函数和协程
  1. 包含yield关键字的函数就是生成器函数
  2. 通过data = yield生成器函数内就可以接收调用方传来的值,接收值的生成器函数,就可以理解为Python的协程

4. 使用协程实现闭包

普通的闭包:我们定义一个嵌套函数来实现数据的加和,外层函数定义一个total变量来记录前面的和:

def add():
    total = 0.0
    def calculate(x):
        nonlocal total
        total += x
        return total
    return calculate

a = add()
print(a(1))	# 1.0
print(a(2))	# 3.0
print(a(3))	# 6.0

使用协程:

def add():
    total = 0.0
    while True:
        term = yield total
        total += term
>>> a = add()
>>> next(a)     # 调用next函数,预激协程
0.0
>>> a.send(1)   # 多次调用send(),计算总和
1.0
>>> a.send(2)   # 多次调用send(),计算总和
3.0
>>> a.send(3)   # 多次调用send(),计算总和
6.0
>>>

5. 让协程返回值

我们想让协程跟普通函数一样,通过return返回值给我们。首先抛出一个疑问,既然调用方可以通过yield接收协程返回的值,我们为什么还要让协程返回值通过return返回值给我们呢?这是因为很多协程不会通过yield产出值,而是在最后返回一个值(比如累计值)。改写上面的累加函数:

def add():
    total = 0.0
    while True:
        term = yield		# yield不再产出值
        if term is None:	# 让协程接收None时结束while True循环
            break
        total += term
    return {'total': total}
>>> a = add()
>>> next(a)
>>> a.send(1)
>>> a.send(2)
>>> a.send(3)
>>> a.send(None)	# send(None)使协程结束while
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: {'total': 6.0}	# 抛出了StopIteration

为什么会抛出StopIteration?生成器结束的时候就会抛出StopIteration,如果还不理解,请回去看生成器的内容。

我们可以使用try…except…捕获这个StopIteration:

>>> a = add()
>>> next(a)
>>> a.send(1)
>>> a.send(2)
>>> a.send(3)
>>> try:
...     a.send(None)
... except StopIteration as e:
...     result = e.value
...
>>> result
{'total': 6.0}

在函数外面写try…except…真的是看得人头疼,所以尝试定义一个函数,帮助我们完成try…except…,但是这个函数又要能够跟生成器一样暂停和接收值,所以这个函数注定是一个生成器函数,是一个协程。

下面实现了一个例子,我们把add()叫做子生成器,把grouper()叫做委派生成器,这俩都是协程:

def add():
    total = 0.0
    while True:
        term = yield
        if term is None:
            break
        total += term
    return {'total': total}

def grouper(result):
    _a = add()
    next(_a)	# 预激子生成器
    while True:
        x = yield	# yield必不可少,接收调用方的值
        try:		
            _a.send(x)	# 委派生成器接收调用方的值后,要把值传给子生成器
        except StopIteration as e:	# 我们只捕获子生成器的StopIteration
            res = e.value
            result.append(res)
            # return res        # 为什么不返回?
            
if __name__ == '__main__':
    result = []
    g = grouper(result)
    next(g)		# g本身也是一个生成器,需要预激活
    g.send(1)
    g.send(2)
    g.send(3)
    g.send(None)
    print(result)
[{'total': 6.0}]

委派生成器grouper帮我们完成了两件事,第一,预激活子生成器(协程),第二,捕获StopIteration的返回值。但是grouper本身也是一个生成器,需要预激活,所以对于调用方而言,唯一不用关心的,就是如何捕获StopIteration的返回值了。

为什么委派生成器grouper不使用返回值,我在这里讲一下自己的理解:

因为grouper也是一个生成器函数,如果返回,则会抛出StopIteration,我们又需要在外部处理StopIteration。而如果不返回,grouper会暂停在yield处,调用方不需要处理StopIteration。为了获取子生成器add的返回值,调用方可以传入可变对象,注意,一定是可变对象。因为可变对象传参,函数修改的是原来的可变对象,而如果是不可变对象传参,函数会拷贝一份不可变对象的值,不会修改原来的不可变对象。如果不是很理解,请自行百度可变对象传参和不可变对象传参的区别。

6. yield from

好家伙,很多文章博客,上来就是yield from贴脸,搞得我一脸懵逼~~~下面讲讲从yield过渡到yield from。

上面的示例,grouper的代码太多了吧!看得头疼,yield from 就是为了解决这个问题的,使用yield from简化grouper的代码:

def grouper(result):
    while True:
        res = yield from add()
        result.append(res)
[{'total': 6.0}]

运行结果和前面是一样的。是不是清爽很多?

yield from的作用,是为了获取子生成器的返回值。假如你在委派生成器grouper内return,调用方依旧会接收到StopIteration。所以最终,总得有人处理这个StopIteration的,或者干脆不抛出StopIteration。

def add():
    total = 0.0
    while True:
        term = yield
        if term is None:
            break
        total += term
    return {'total': total}

def middle():
    res = yield from add()
    return {'result': res}

def grouper():
    res = yield from middle()
    return res

if __name__ == '__main__':
    result = []
    g = grouper()
    next(g)
    g.send(1)
    g.send(2)
    g.send(3)
    try:
        g.send(None)
    except StopIteration as e:
        print(e.value)

7. 再使用yield from实现多层委派生成器

上面的返回是[{'total': 6.0}],我们加个中间层的委派生成器,使得返回值变为[{'result': {'total': 6.0}}]

def add():
    total = 0.0
    while True:
        term = yield
        if term is None:
            break
        total += term
    return {'total': total}

def middle():		# 细节1:中间的委派生成器一定要返回值
    res = yield from add()
    return {'result': res}	# return

def grouper(result):	# 接收一个可变对象用于保存返回值
    while True:		# 细节2:末层的委派生成器使用while True循环,不return抛出异常
        res = yield from middle()
        result.append(res)

if __name__ == '__main__':
    result = []
    g = grouper(result)
    next(g)
    g.send(1)
    g.send(2)
    g.send(3)
    g.send(None)
    print(result)
[{'result': {'total': 6.0}}]

委派生成器相当于管道,可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达式的简单生成器结束。

8. 使用协程模拟出租车队运营程序

终于来到这里了,使用协程实现出租车程序调度,帮助我们理解协程的调度是如何实现并发的。前面内容写累了,此处就不做讲解了,直接翻书看,例子来自《流畅的Python》。贴上代码和运行结果:

import random
import collections
import queue

DEFAULT_NUMBER_OF_TAXIS = 3
DEFAULT_END_TIME = 180
SEARCH_DURATION = 5
TRIP_DURATION = 20
DEPARTURE_INTERVAL = 5

Event = collections.namedtuple('Event', 'time proc action')


# BEGIN TAXI_PROCESS
def taxi_process(ident, trips, start_time=0):  # <1>
    """Yield to simulator issuing event at each state change"""
    time = yield Event(start_time, ident, 'leave garage')  # <2>
    for i in range(trips):  # <3>
        time = yield Event(time, ident, 'pick up passenger')  # <4>
        time = yield Event(time, ident, 'drop off passenger')  # <5>

    yield Event(time, ident, 'going home')  # <6>
    # end of taxi process # <7>
# END TAXI_PROCESS


# BEGIN TAXI_SIMULATOR
class Simulator:

    def __init__(self, procs_map):
        self.events = queue.PriorityQueue()
        self.procs = dict(procs_map)

    def run(self, end_time):  # <1>
        """Schedule and display events until time is up"""
        # schedule the first event for each cab
        for _, proc in sorted(self.procs.items()):  # <2>
            first_event = next(proc)  # <3>
            self.events.put(first_event)  # <4>

        # main loop of the simulation
        sim_time = 0  # <5>
        while sim_time < end_time:  # <6>
            if self.events.empty():  # <7>
                print('*** end of events ***')
                break

            current_event = self.events.get()  # <8>
            sim_time, proc_id, previous_action = current_event  # <9>
            print('taxi:', proc_id, proc_id * '   ', current_event)  # <10>
            active_proc = self.procs[proc_id]  # <11>
            next_time = sim_time + compute_duration(previous_action)  # <12>
            try:
                next_event = active_proc.send(next_time)  # <13>
            except StopIteration:
                del self.procs[proc_id]  # <14>
            else:
                self.events.put(next_event)  # <15>
        else:  # <16>
            msg = '*** end of simulation time: {} events pending ***'
            print(msg.format(self.events.qsize()))
# END TAXI_SIMULATOR


def compute_duration(previous_action):
    """Compute action duration using exponential distribution"""
    if previous_action in ['leave garage', 'drop off passenger']:
        # new state is prowling
        interval = SEARCH_DURATION
    elif previous_action == 'pick up passenger':
        # new state is trip
        interval = TRIP_DURATION
    elif previous_action == 'going home':
        interval = 1
    else:
        raise ValueError('Unknown previous_action: %s' % previous_action)
    return int(random.expovariate(1/interval)) + 1


def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
         seed=None):
    """Initialize random generator, build procs and run simulation"""
    if seed is not None:
        random.seed(seed)  # get reproducible results

    taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL)
             for i in range(num_taxis)}
    sim = Simulator(taxis)
    sim.run(end_time)


if __name__ == '__main__':
    main()
taxi: 0  Event(time=0, proc=0, action='leave garage')
taxi: 1     Event(time=5, proc=1, action='leave garage')
taxi: 0  Event(time=6, proc=0, action='pick up passenger')
taxi: 1     Event(time=6, proc=1, action='pick up passenger')
taxi: 0  Event(time=8, proc=0, action='drop off passenger')
taxi: 0  Event(time=9, proc=0, action='pick up passenger')
taxi: 2        Event(time=10, proc=2, action='leave garage')
taxi: 2        Event(time=13, proc=2, action='pick up passenger')
taxi: 2        Event(time=14, proc=2, action='drop off passenger')
taxi: 0  Event(time=16, proc=0, action='drop off passenger')
taxi: 2        Event(time=17, proc=2, action='pick up passenger')
taxi: 0  Event(time=19, proc=0, action='going home')
taxi: 2        Event(time=19, proc=2, action='drop off passenger')
taxi: 1     Event(time=22, proc=1, action='drop off passenger')
taxi: 1     Event(time=26, proc=1, action='pick up passenger')
taxi: 1     Event(time=31, proc=1, action='drop off passenger')
taxi: 1     Event(time=32, proc=1, action='pick up passenger')
taxi: 1     Event(time=39, proc=1, action='drop off passenger')
taxi: 2        Event(time=39, proc=2, action='pick up passenger')
taxi: 1     Event(time=42, proc=1, action='pick up passenger')
taxi: 1     Event(time=55, proc=1, action='drop off passenger')
taxi: 1     Event(time=56, proc=1, action='going home')
taxi: 2        Event(time=63, proc=2, action='drop off passenger')
taxi: 2        Event(time=66, proc=2, action='pick up passenger')
taxi: 2        Event(time=92, proc=2, action='drop off passenger')
taxi: 2        Event(time=105, proc=2, action='pick up passenger')
taxi: 2        Event(time=155, proc=2, action='drop off passenger')
taxi: 2        Event(time=156, proc=2, action='pick up passenger')
taxi: 2        Event(time=171, proc=2, action='drop off passenger')
taxi: 2        Event(time=175, proc=2, action='going home')
*** end of events ***

期待再会…

[Python] 使用futures模块处理并发(超好用的并发库)

future和asyncio还没讲完,后续内容会慢慢更新。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值