# 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!

reCAPTCHA protected (Google Privacy Policy & TOS)