Python 3, an Illustrated Tour Transcripts
Chapter: Asynchronous Programming
Lecture: Cooperative Multitasking From the Ground Up
0:00 I'm going to go through some code here just to give you some insight into how you could make your own asynchronous library
0:09 and I didn't come up with this, this was from a presentation I saw from Robert Smallshire, and so I'll credit him on this, this isn't his code,
0:17 but this is adapted code from him, but I really like the approach here to give you some insight into developing an asynchronous framework.
0:25 Here, I've got a function called map, and if you're familiar with functional programming, map takes a function and a sequence
0:31 and it applies that function to every item in the sequence. So this should be pretty straightforward. We're creating a result list here
0:37 and we're just looping over our sequence and we're appending into that.
0:41 We could use a list comprehension or whatnot for this, but bare with me for a minute,
0:44 if we wanted to make this asynchronous, then we've got to change it a little bit it's got to yield, it's got to give something else the chance to run
0:52 and so I'm going to put a yield in here and I'm going to change it to say async map here.
0:57 So this looks very similar, I just put a yield in there and it says after I do some work, yield give something else the ability to run.
1:03 So if you're familiar with generators, you'll know that generators in Python allow you to call them
1:08 and once you call them and they get to this yield point they freeze their state and then when you call next on them,
1:14 when you're looping over a generator, you can resume the state exactly where it left off. So we're going to change this function into a generator
1:23 so that we can loop over it and we can call next on it. So here's an example of doing that here.
1:28 Our function is a generator and we're going to make an instance of this generator, we're going to pass in a lambda in there that just adds to something
1:35 and we're going to pass in range of 3 so 0 up not including 3 and if you're familiar with the iteration protocol, how for loops work under the covers,
1:44 basically you get an iterator and then you call next on that iterator. Well generator in Python is an iterator and so you can call next on a generator.
1:52 So I'm going to call next on it and that's going to say okay, I'm going to apply lambda to the first guy in range.
2:00 The first guy in range is zero the lambda adds 2 to zero, and at that point it's going to append that and it's going to yield
2:07 and so it yielded, it appended into our result that guy, the 2 and then we're going to call next again and it will put three in that result
2:17 and note that it's giving me back my interpreter here. I have my interpreter back, I'm doing this from the console here
2:23 and maybe I do some other work, I can say 5+7. Well, that's 12, and now I want to go back and I want to do this again.
2:30 So I call next again and it does some more work and it's going to stick 4 on the end, the iteration protocol when I call next again, I'm done,
2:37 It's going to give me a stop iteration but note that since I returned the result list that comes in my exception here.
2:44 So the stop iteration also has my result at the end of it. So that's pretty cool, so I can take that concept here and make a function from it now.
2:53 So I'm going to make a function called runner that takes a generator and it's just going to go into a while loop and it's going to call next repeatedly
2:59 until I get a stop iteration and when I get to stop iteration exception, I'm going to say well pull off the value guy
3:05 because that's going to be whatever the generator returned. And we can run that here and we can see that
3:10 when I pass in the same code here that I had previously I pass it into my runner function, I get that result out of it.
3:17 Pretty cool, but what this is allowing me to do is right after this next here, I could work on something else
3:23 or have multiple generators that I'm working on at the same time. So in order to do that, we're going to make a class called a task
3:31 a task will wrap a generator, we're just going to pass a generator into it and it will have an ID.
3:36 So we're going to just take an ID and keep track of our generator as instances in there. The ID is tracked as a class variable here.
3:45 Okay, so now we have a task, let's make a scheduler, a scheduler is going to take task and it's going to run them.
3:52 We're going to import from the collections module, let's call the deck or the double ended queue.
3:57 This allows us to efficiently stick things in the front and in the back and pull them off at either end very quickly.
4:03 What we're going to do is stick tasks into our deck pull them off the front and then stick them back into the other end as we're working on them.
4:10 So we're going to have a deck here with our tasks in it and we're going to make 2 other attributes, results and exceptions,
4:15 those are both dictionaries, they're going to map the task ID to either the result that came out of it or if there is an exception
4:22 they're going to map the task to the exception. And then we have an add method and add method takes a generator
4:27 and it just sticks it into our tasks list after it wraps it with the task class. Now, we have the run method,
4:34 this is where the meat of our scheduler is. It's just going to be very similar to what we saw before here.
4:39 We're going to have a while loop it's going to have an infinite loop. It's going to say if we don't have any tasks pop out of there.
4:45 Otherwise, what we're going to do is we're going to get our first task, our T from the left hand side and we're going to call next on it.
4:52 We're going to print out that we're running it, but we're going to call next on it. So it's going to do some work until it gets to that yield,
4:58 and it might have other things that happen, we might get a stop iteration. So we might be done with that generator.
5:03 If we did get a stop iteration that indicates that we're done and we're going to stick into our results dictionary whatever we got for the value there.
5:11 We might also get an exception, if some exception happen so we can just remember our exception.
5:16 If our generator is still running, so we didn't get a stop iteration or an exception, we're going to stick it back into the end of this deck.
5:22 So it's going to come back in the other end and then we're going to come back up to the top here and we're going to get our next task here
5:29 and we're going to work on that one, and we'll just keep working on these and they're all going to yield or they should yield
5:34 and allow other tasks to work at the same time. And at some point, all the tasks will be done, we'll break out of this.
5:40 So let's just look at an example of running this here. I'm going to make two generators async map g1 and another one async map
5:49 one is adding to the other, one has a lambda that is multiplying by 3 and they have slightly different input sizes,
5:56 we'll make an instance of our scheduler and we'll just add those two generators to them
6:00 and then we'll call run and we'll see that run is switching off between the two it's going to say I'm running 1 now, I'm running 2, now I'm running 1,
6:06 and at some point 1, which is only three long, gets finished and it says this is the result of running that
6:13 and it keeps working and now it's just working on 2, and then it gets the result of 2.
6:18 And finally it's done, we can say what are the results of the scheduler and it says well the results from task 1 are this
6:26 and the results of task 2 are that, but note that it interwove those results. It worked on both of them at the same time.
6:33 Hopefully, that gives you some insight into how you can yield and allow something else to run and then come back and work on something else.
6:41 So as long as we have these yields or awaits in Python 3.6 in our coroutines that we're creating, we can take advantage of this asyncio framework