Question: How to remove all for loops from our code?
A: by manually using iter and next to loop over iterables.
In the Python world, an iterable is any object that you can loop over with a for loop, like list, dictionary, tuple, etc.
Iterables are not always indexable, they don’t always have lengths, and they’re not always finite.
from itertools import count
multiples_of_five = count(step=5)
for n in multiples_of_five:
if n > 100:
break
print(n)
All iterables can be passed to the built-in iter function to get an iterator from them.
>>> iter(['some', 'list'])
>>> iter({'some', 'set'})
>>> iter('some string')
Iterators have exactly one job: return the “next” item in our iterable. You can get an iterator from any iterable
>>> iterator = iter('hi')
>>> next(iterator)
'h'
>>> next(iterator)
'i'
>>> next(iterator)
Traceback (most recent call last):
File "", line 1, in
StopIteration
So calling iter on an iterable gives us an iterator. And calling next on an iterator gives us the next item or raises a StopIteration exception if there aren’t any more items.
You can pass iterators to the built-in iter function to get themselves back. That means that iterators are also iterables.
>>> iterator = iter('hi')
>>> iterator2 = iter(iterator)
>>> iterator is iterator2
True
This print_each function loops over some iterable, printing out each item as it goes:
def print_each(iterable):
iterator = iter(iterable)
while True:
try:
item = next(iterator)
except StopIteration:
break # Iterator exhausted: stop the loop
else:
print(item)
This for loop is automatically doing what we were doing manually: calling iter to get an iterator and then calling next over and over until a StopIteration exception is raised.
Question: how you can build your own iterator?
Answer: using iter and next methods.
Iterators are everywhere in Python. They are elegantly implemented within for loops, comprehensions, generators etc. but hidden in plain sight.
Iterator in Python is simply an object that can be iterated upon. An object which will return data, one element at a time.
Technically speaking, Python iterator object must implement two special methods, iter() and next(), collectively called the iterator protocol.
An object is called iterable if we can get an iterator from it. Most of built-in containers in Python like: list, tuple, string etc. are iterables.
The iter() function (which in turn calls the iter() method) returns an iterator from them.
next(my_iter) is the same as my_iter.next().
The for loop implementation:
# create an iterator object from that iterable
iter_obj = iter(iterable)
# infinite loop
while True:
try:
# get the next item
element = next(iter_obj)
# do something with element
except StopIteration:
# if StopIteration is raised, break from loop
break
Building an iterator from scratch is easy in Python. We just have to implement the methods iter() and next().
Here, we show an example that will give us next power of 2 in each iteration. Power exponent starts from zero up to a user set number.
class PowTwo:
"""Class to implement an iterator
of powers of two"""
def __init__(self, max = 0):
self.max = max
def __iter__(self):
self.n = 0
return self
def __next__(self):
if self.n <= self.max:
result = 2 ** self.n
self.n += 1
return result
else:
raise StopIteration
Now we can create an iterator and iterate through it as follows.
>>> a = PowTwo(4)
>>> i = iter(a)
>>> next(i)
1
>>> next(i)
2
>>> next(i)
4
>>> next(i)
8
>>> next(i)
16
>>> next(i)
Traceback (most recent call last):
...
StopIteration
We can also use a for loop to iterate over our iterator class.
>>> for i in PowTwo(5):
... print(i)
...
1
2
4
8
16
32
Question: how to create an Infinite Iterators?
The built-in function iter() can be called with two arguments where the first argument must be a callable object (function) and second is the sentinel. The iterator calls this function until the returned value is equal to the sentinel.
>>> int()
0
>>> inf = iter(int,1)
>>> next(inf)
0
>>> next(inf)
0
We can see that the int() function always returns 0. So passing it as iter(int,1) will return an iterator that calls int() until the returned value equals 1. This never happens and we get an infinite iterator.
class InfIter:
"""Infinite iterator to return all
odd numbers"""
def __iter__(self):
self.num = 1
return self
def __next__(self):
num = self.num
self.num += 2
return num
>>> a = iter(InfIter())
>>> next(a)
1
>>> next(a)
3
>>> next(a)
5
>>> next(a)
7
The advantage of using iterators is that they save resources. Like shown above, we could get all the odd numbers without storing the entire number system in memory. We can have infinite items (theoretically) in finite memory.
Question: How to create iterations easily using Python generators?
Simply speaking, a generator is a function that returns an object (iterator) which we can iterate over (one value at a time).
If a function contains at least one yield statement (it may contain other yield or return statements), it becomes a generator function. Both yield and return will return some value from a function.
The difference is that, while a return statement terminates a function entirely, yield statement pauses the function saving all its states and later continues from there on successive calls.
Methods like iter() and next() are implemented automatically. So we can iterate through the items using next(). Finally, when the function terminates, StopIteration is raised automatically on further calls.
Once the function yields, the function is paused and the control is transferred to the caller.
Unlike normal functions, the local variables are not destroyed when the function yields. Furthermore, the generator object can be iterated only once.
# A simple generator function
def my_gen():
n = 1
print('This is printed first')
# Generator function contains yield statements
yield n
n += 1
print('This is printed second')
yield n
n += 1
print('This is printed at last')
yield n
for item in my_gen():
print(item)
This is printed first
1
This is printed second
2
This is printed at last
3
# Intialize the list
my_list = [1, 3, 6, 10]
a = (x**2 for x in my_list)
# Output: 1
print(next(a))
# Output: 9
print(next(a))
# Output: 36
print(next(a))
# Output: 100
print(next(a))
# Output: StopIteration
next(a)
Generators can be used to pipeline a series of operations. This is best illustrated using an example.
Suppose we have a log file from a famous fast food chain. The log file has a column (4th column) that keeps track of the number of pizza sold every hour and we want to sum it to find the total pizzas sold in 5 years.
Assume everything is in string and numbers that are not available are marked as 'N/A'. A generator implementation of this could be as follows.
with open('sells.log') as file:
pizza_col = (line[3] for line in file)
per_hour = (int(x) for x in pizza_col if x != 'N/A')
print("Total pizzas sold = ",sum(per_hour))
Reference:
- http://treyhunner.com/2016/12/python-iterator-protocol-how-for-loops-work/
- https://www.programiz.com/python-programming/iterator
- https://www.programiz.com/python-programming/generator