Iterators and generators

To understand how asynchronous programming works in Python, it is important to first understand how iterators and generators work because they are the basis of asynchronous features in Python.

An iterator in Python is a class that implements the Iterator protocol. The class must implement the following two methods:

  • __iter__(): ; ;Returns the actual iterator. It often returns self
  • next(): ;Returns the next value until StopIteration() is raised

In the following example, we'll implement the Fibonacci sequence as an iterator:

    class Fibo: 
        def __init__(self, max=10): 
            self.a, self.b = 0, 1 
            self.max = max 
            self.count = 0 
 
        def __iter__(self): 
            return self 
 
        def next(self): 
            try: 
                return self.a 
            finally: 
                if self.count == self.max: 
                    raise StopIteration() 
                self.a, self.b = self.b, self.a + self.b 
                self.count += 1

Iterators can be used directly in loops, as follows:

>>> for number in Fibo(10): 
...     print(number) 
... 
0 
1 
1 
2 
3 
5 
8
13 
21 
34

To make iterators more Pythonic, generators were added to Python. They have introduced the yield keyword. When yield is used by a function instead of return, this function is converted into a generator. Each time the yield keyword is encountered, the function returns the yielded value and pauses its execution.

    def fibo(max=10): 
        a, b = 0, 1 
        cpt = 0 
        while cpt < max: 
            yield a 
            a, b = b, a + b 
            cpt += 1 

This behavior makes generators a bit similar to coroutines found in other languages, except that coroutines are bidirectional. They return a value as yield does, but they can also receive a value for its next iteration.

Being able to pause the execution of a function and communicate with it both ways is the basis for asynchronous programming--once you have this ability, you can use an event loop, and pause and resume functions.

The yield call was extended to support receiving values from the caller via the sender() method. In the next example, a terminal() function simulates a console, which implements three instructions, echo, exit, and eval:

    def terminal(): 
        while True: 
            msg = yield    # msg gets the value sent via a send() call 
            if msg == 'exit': 
                print("Bye!") 
                break 
            elif msg.startswith('echo'): 
                print(msg.split('echo ', 1)[1]) 
            elif msg.startswith('eval'): 
                print(eval(msg.split('eval', 1)[1])) 

When instantiated, this generator can receive data via its send() method:

>>> t = terminal() 
>>> t.next()    # call to initialise the generator - similar to send(None) 

>>> t.send("echo hey") hey
>>> t.send("eval 1+1") 2
>>> t.send("exit") Bye! Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

Thanks to this addition, Python generators became similar to coroutines.

Another extension that was added to yield is yield from, which lets you chain-call another generator.

Consider the following example, where a generator is uses two other generators to yield values:

    def gen1(): 
        for i in [1, 2, 3]: 
            yield i 
 
    def gen2(): 
        for i in 'abc': 
            yield i 
 
    def gen(): 
        for val in gen1(): 
            yield val 
        for val in gen2(): 
            yield val 

The two for loops in the gen() function can be replaced by a single yield from call as follows:

    def gen(): 
        yield from gen1() 
        yield from gen2() 

Here's an example of calling the gen() method until each sub generator gets exhausted:

>>> list(gen()) 
[1, 2, 3, 'a', 'b', 'c'] 

Calling several other coroutines and waiting for their completion is a prevalent pattern in asynchronous programming. It allows developers to split their logic into small functions and assemble them in sequence. Each yield call is an opportunity for the function to pause its execution and let another function take over.

With these features, Python got one step closer to supporting asynchronous programming natively. Iterators and generators were used as building blocks to create native coroutines.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset