Lazy Looping Helpers¶
Iterators are Everywhere¶
Many of Python built-in functions return iterators instead of lists.
The enumerate
and reversed
functions return iterators. Files in Python are also iterators.
>>> letters = ['a', 'b']
>>> next(enumerate(letters))
(0, 'a')
>>> next(reversed(letters))
'b'
>>> next(open('widgets.txt'))
'WIDGET READING 1\n'
All of these iterators act like lazy iterables. They don’t do any work, until you start looping over them.
In Python 2, many functions like range
returned lists; if you wanted an iterator, you had to use a special version of the function.
In Python 3, the special versions like xrange
are removed and wherever it makes sense, iterators are returned instead.
Many functions built-in to Python also accept generic iterables, including lazy iterators.
For example enumerate
and zip
both accept any kind of iterables, including iterators:
>>> numbers = [1, 2, 3]
>>> squares = (n**2 for n in numbers)
>>> squares
<generator object <genexpr> at 0x100583b10>
>>> zip(numbers, squares)
<zip object at 0x1005ce2c8>
>>> list(zip(numbers, squares))
[(1, 1), (2, 4), (3, 9)]
Iterators in the Standard Library¶
The standard library includes a number of functions which are meant to work with lazy iterables as well as functions that return lazy iterables, usually in the form of iterators.
The itertools
library includes many such functions:
The itertools.count
function produces continuous incremental numbers forever.
If you want to print out every number for the rest of time, do this:
>>> from itertools import count
>>> for x in count():
>>> print(x)
...
We could get an infinite list of square numbers like this:
>>> squares = (n**2 for n in count())
You have to be careful with count
, or it will go on forever. It is important to make sure that there is some way of stopping the loop.
If we wanted just the first 10 squares we could try to slice this generator:
>>> squares[:10]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
But we’ll get an error because generators are not indexable and therefore not sliceable.
We could use itertools.islice
to slice any iterable instead:
>>> from itertools import islice
>>> squares = (n**2 for n in count())
>>> list(islice(squares, 10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
We could also use itertools.takewhile
to get all the squares which are less than 1000:
>>> from itertools import takewhile
>>> squares = (n**2 for n in count())
>>> def less_than_1000(n): return n < 1000
...
>>> list(takewhile(less_than_1000, squares))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961]
Embracing Laziness¶
Anytime you come across something that creates an entire list, only to loop through the list and throw it away later, consider lazy iterables. Either with generator expressions or generator functions, you can potentially save space and improve the efficiency of a program by making the looping lazy.
itertools Exercises¶
Except as noted, these exercises are in the iteration.py
file in the exercises
directory. Edit the file to add the functions or fix the error(s) in the existing function(s).
To run the test: from the exercises
folder, type python test.py <function_name>
, like this:
$ python test.py get_primes_over
Head¶
Edit the head
function we saw before in functions.py
so that it returns an iterator which gives the first n
items of a given iterable.
Use something from itertools
to solve this exercise.
Example:
>>> from generators import head
>>> list(head([1, 2, 3, 4, 5], n=2))
[1, 2]
>>> first_4 = head([1, 2, 3, 4, 5], n=4)
>>> list(zip(first_4, first_4))
[(1, 2), (3, 4)]
All Together¶
Edit the all_together
function we saw before in generators.py
so that it takes any number of iterables and strings them together.
Use something from itertools
to solve this exercise.
Example:
>>> from generators import all_together
>>> list(all_together([1, 2], (3, 4), "hello"))
[1, 2, 3, 4, 'h', 'e', 'l', 'l', 'o']
>>> nums = all_together([1, 2], (3, 4))
>>> list(all_together(nums, nums))
[1, 2, 3, 4]
Total Length¶
Edit the total_length
function so that it calculates the total length of all given iterables.
Example:
>>> from iteration import lotal_length
>>> total_length([1, 2, 3])
3
>>> total_length()
0
>>> total_length([1, 2, 3], [4, 5], iter([6, 7]))
7
lstrip¶
Edit the lstrip
function to accept an iterable and an item to strip from the beginning of the iterable.
The lstrip
function should return an iterator which, when looped over, will provide each of the items in the given iterable after all of the strip values have been removed from the beginnign.
Example:
>>> list(lstrip([0, 0, 1, 0, 2, 3, 0], 0))
[1, 0, 2, 3, 0]
>>> ''.join(lstrip('hhello there' 'h'))
'ello there'
Stop On¶
Edit the stop_on
function we saw before in functions.py
so that it accepts an iterable and a value and yields from the given iterable repeatedly until the given value is reached. Use something from itertools
to solve this exercise.
Example:
>>> from functions import stop_on
>>> list(stop_on([1, 2, 3], 3))
[1, 2]
>>> next(stop_on([1, 2, 3], 1), 0)
0
Interleave¶
Edit the interleave
function we saw before in the generators.py
file so that it works with iterables of different length.
Any short iterables should be skipped over once exhausted.
For example:
>>> list(interleave([1, 2, 3], [6, 7, 8, 9]))
[1, 6, 2, 7, 3, 8, 9]
>>> list(interleave([1, 2, 3], [4, 5, 6, 7, 8]))
[1, 4, 2, 5, 3, 6, 7, 8]
>>> list(interleave([1, 2, 3, 4], [5, 6]))
[1, 5, 2, 6, 3, 4]
Note: to test this exercise you’ll need to comment out the appropriate @unittest.skip
line in generators_test.py
.
Big Primes¶
Edit the get_primes_over
function we saw before in the functions.py
file so that it yields a specified number of prime numbers greater than 1,000,000.
Try doing this without using while
loops or for
loops, using itertools
.
You can use this function to determine whether a number is prime:
def is_prime(candidate):
"""Return True if candidate is a prime number"""
for n in range(2, candidate):
if candidate % n == 0:
return False
return True
I send out 1 Python exercise every week through a Python skill-building service called Python Morsels.
If you'd like to improve your Python skills every week, sign up!
You can find the Privacy Policy here.reCAPTCHA protected (Google Privacy Policy & TOS)