Coroutines in Python go through the following three phases:

  1. The original generator variant yield/send
  2. Introduce @asyncio.coroutine and yield from
  3. Python3.5 introduced the async/await keyword

Yield /send

If the yield keyword appears in a normal function, the function is no longer a normal function, but a generator.

def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        yield alist.pop(c)
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
Copy the code

< Generator object mygen at 0x02E5BF00>

C, like the code above, is a generator. A generator is an iterator that can be iterated over using for. The most important feature of a generator function is that it can take a variable passed in from the outside and return the result calculated according to the variable content. This is all done by the send() function inside the generator.

def gen():
    value=0
    while True:
        receive=yield value
        if receive=='e':
            break
        value = 'got: %s' % receive

g=gen()
print(g.send(None))    
print(g.send('hello'))
print(g.send(123456))
print(g.send('e'))
Copy the code

Receive =yield Value the most critical and confusing of the generator functions above is the receive=yield value sentence. If you misunderstand the execution steps of the loop body, you will miss by a mile.

Receive =yield Value consists of three steps:

1. Throw (return) value out of the function

2. Pause and wait for next() or send() to resume

Assign receive=MockGetValue().

MockGetValue() is an imaginary function that receives the value sent by Send ()

Execution process:

Start the generator function with g.end (None) or next(g) and execute to the end of the first yield statement. This is the key, this is where a lot of people get confused. When we run the receive=yield Value statement, the program only performs one or two steps, returns value, pauses, and does not perform step 3 to assign a value to receive. So the yield value will output the initial value 0. Note here: you can only send(None) when starting a generator function, and you will get an error message if you try to enter any other value.

2. By passing g.end (‘hello’), hello is passed in and continues execution from where it was paused last time, so run step 3 and assign to receive. It then evaluates the value of the value and returns to the while header. At yield value, the program executes 1,2 steps again. The program returns the value and pauses. The yield value will output “got: hello” and wait for send() to activate.

G.end (123456) : got: 123456

4. When we g.end (‘ e ‘), the program will execute break and push out the loop until the function completes, so it will get StopIteration.

As you can see from the above, after the first send(None) starts the generator (execute 1 — >2, usually the value returned the first time is not useful), the generator actually runs in the loop 3 — >1 — >2 for each external send(). Then return a value and pause the wait.

Second, the yield of the from

Take a look at this code:

def g1():     
     yield  range(5)
def g2():
     yield  from range(5)

it1 = g1()
it2 = g2()
for x in it1:
    print(x)

for x in it2:
    print(x)
Copy the code

Range (0, 5) 0 1 2 3 4

This tells us that yield simply returns the range iterable. Yield from parses the range object, returning each item. Yield from iterable is essentially equal to for item in iterable: a shortened version of yield item. Let’s take an example. Suppose we’ve written a Fibonacci sequence function

Def fab(Max): n,a,b = 0,0,1 def fab(Max): n,a,b = 0,0,1Copy the code

Fab is not a normal function, but a generator. So Fab (5) does not execute a function, but returns a generator object (the generator must be an iterator, and the iterator must be an iterable). Now let’s see, if we want to implement a function based on Fab (), the call starts with a log

def f_wrapper(fun_iterable):
    print('start')
    for item  in fun_iterable:
        yield item
     print('end')
wrap = f_wrapper(fab(5))
for i in wrap:
    print(i,end=' ')
Copy the code

Now use yield from instead of the for loop

import logging def f_wrapper2(fun_iterable): Print ('end') wrap = f_wrapper2(fab(5)) for I in wrap: print(i,end=' ')Copy the code

Once again: yield from must be followed by an iterable (generator, iterator)

Asyncio. Coroutine and yield from

Yield from thrives in asyncio modules. We used to switch coroutines manually, but now when we declare a function as a coroutine, we schedule the coroutine through an event loop.

Take a look at the sample code:

import asyncio,random @asyncio.coroutine def smart_fib(n): index = 0 a = 0 b = 1 while index < n: sleep_secs = random.uniform(0, Sleep (sleep_secs) # print('Smart one think {} secs to get {}'.format(sleep_secs, b)) a, b = b, a + b index += 1 @asyncio.coroutine def stupid_fib(n): index = 0 a = 0 b = 1 while index < n: sleep_secs = random.uniform(0, Sleep (sleep_secs) # print('Stupid one think {} secs to get {}'.format(sleep_secs, b)) a, b = b, a + b index += 1 if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [ smart_fib(10), stupid_fib(10), ] loop.run_until_complete(asyncio.wait(tasks)) print('All fib finished.') loop.close()Copy the code

The yield from syntax allows us to easily invoke another generator. In this case, the asyncio.sleep() followed by yield from is a coroutine(which also uses yield from), so the thread does not wait for asyncio.sleep(), but interrupts and executes the next message loop. When asyncio.sleep() returns, the thread can get the return value (None in this case) from yield and proceed to the next line.

Asyncio is a module that implements asynchronous I/O based on an event loop. With yield from, we can give control of the coroutine asyncio.sleep to the event loop and suspend the current coroutine; After that, the event loop decides when to wake up asyncio.sleep and then executes the code backwards. Scheduling between coroutines is determined by event loops.

Sleep (1) cannot be used here because time.sleep() returns None, which is not iterable. Remember that yield from must be followed by iterable objects (generators, iterators). So an error will be reported:

yield from time.sleep(sleep_secs)

TypeError: ‘NoneType’ object is not iterable

Async and await

Once you understand asyncio.coroutine and yield from, the async and await introduced in Python3.5 are easy to understand: you can think of them as perfect proxies for asyncio.coroutine/yield from. Of course, from a Python design point of view, async/await allows coroutines to exist ostensibly separate from generators, hiding the details under the Asyncio module and making the syntax clearer. The new keyword async can turn any ordinary function into a coroutine

import time,asyncio,random
async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        print(alist.pop(c))
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
Copy the code

<coroutine object mygen at 0x02C6BED0>

In the above program, we add async in front of the function to make it a coroutine.

But async is not valid for generators. Async cannot convert a generator to a coroutine. So once again, let’s change print to yield

async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        yield alist.pop(c)
a = ["ss","dd","gg"]
c=mygen(a)
print(c)
Copy the code

You can see the output

<async_generator object mygen at 0x02AA7170> is not a Coroutine object

So our coroutine code should look something like this

import time,asyncio,random async def mygen(alist): while len(alist) > 0: c = random.randint(0, Len (alist)-1) print(alist. Pop (c)) await asyncio.sleep(1) strList =["ss","dd","gg"] intList =[1,2,5,6] c1=mygen(strList) c2=mygen(intlist) print(c1)Copy the code

To run a coroutine, add an event loop to the code above:

if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [
        c1,
        c2
        ]
        loop.run_until_complete(asyncio.wait(tasks))
        print('All fib finished.')
        loop.close()
Copy the code

You can see the effect of alternate execution.