Iterators, Generators and Lazy Evaluation
1. How iterators work
When Python loops through an object, what happens is:
1. The for statement calls iter()
on the object, returning an iterator
2. This iterator has a __next__()
method which accesses the elements in the object
3. When there are no more objects, __next__()
raises a StopIteration
exception
>>> s = 'abc' >>> it = iter(s) >>> it <iterator object at 0x00A1DB50> >>> next(it) 'a' >>> next(it) 'b' >>> next(it) 'c' >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in ? next(it) StopIteration
2. Creating an iterator
Define an __iter__()
method which returns an object with a __next__()
method. (Here it retuns self
since the class itself defines __next__()
.
class Reverse: def __init__(self, data): self.data = data self.index = len(data) def __iter__(self): return self def __next__(self): if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index] rev = Reverse('lol') for char in rev: print(char)
3. Creating an iterator using generators
A generator is a function that returns stuff before it is finished (using yield
), and can be resumed from where they left off (using next
), until it hits StopIteration
Some examples:
>>> def myGen(n): ... yield n + 1 ... yield n + 2 ... >>> g = myGen(0) >>> next(g) 1 >>> next(g) 2 >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Looping through all natural numbers:
>>> def infinite(): ... i = 0 ... while True: ... yield i ... i += 1
The same code for reversing a string as before
def reverse(data): for index in range(len(data)-1, -1, -1): yield data[index]
4. Generator expressions
Generator expressions is much like list comprehensions, but the object is evaluated when it is needed and not when it is created.
gen = (x for x in range(999999))
This saves a ton of memory, since it doesn’t create the whole list upfront.
5. Generator VS generator expressions
They are basically the same thing. Trivia example:
# generator def uc_gen(text): for char in text: yield char.upper() # generator expression def uc_genexp(text): return (char.upper() for char in text)
6. Lazy evaluation, or why do I need generators
Lazy evaluation is an evaluation strategy which delays the evaluation of an expression until its value is needed.
Typical useage: I have to compute a large set of results but I don’t know if I’m going to need them all; or maybe I just don’t want to allocate the memory for all the results at the same time; I have a function that takes long time to execute (think search on a huge domain) and I want to show intermediate results while I get them, instead of waiting for the search to be over.
7. Using generators to loop through a linked list
def looper(self): n = self.head while n is not None: yield n n = n.next # how cool is that? x = [ n.value for n in l.looper() if n.value > 10 ]
8. Using generators to do a inorder() traversal of a BST
# Python 2 def inorder(t): if t: for key in inorder(t.left): yield key yield t.key for key in inorder(t.right): yield key
Python 3 adds some neat syntax:
# Python 3 def inorder(t): if t: yield from inorder(t.left) yield t.key yield from inorder(t.right)