# Generator Expressions¶

## Sum¶

Python has a number of built-in functions that act on iterables. List comprehensions return an iterable so we can pass list comprehensions straight into a one of these functions.

Let’s try out the `sum` function:

```>>> numbers = [1, 2, 3, 4]
>>> sum(numbers)
10
```

Let’s sum the squares of all of the numbers in our `numbers` list. We can use a list comprehension:

```>>> sum([n ** 2 for n in numbers])
30
```

Cool!

## Generator Expressions¶

We can use `sum` with tuples, sets, and any other iterable:

```>>> sum((8, 9, 7))
24
>>> sum({8, 9, 7})
24
```

Sometimes we don’t really care if a list comprehension returns a list, or some other kind of iterable. When we passed a list comprehension into `sum`, we only really needed to pass in an iterable, not necessarily a list.

Let’s use a generator expression instead of a list comprehension. We can make a generator expression like this:

```>>> squares = (n ** 2 for n in numbers)
>>> squares
<generator object <genexpr> at 0x7f733d4f7e10>
```

We can use a generator expression in our `sum` call like this:

```>>> sum((n ** 2 for n in numbers))
30
```

When our generator expression is already in parentheses, we can leave off the redundant parentheses:

```>>> sum(n ** 2 for n in numbers)
30
```

## Refactoring for Efficiency¶

Let’s refactor our code for storing reversible items from a `dictionary file`.

In our last iteration, we have three list comprehensions, and one set.

```with open('dictionary.txt') as dictionary_file:
words = [
line.rstrip()
for line in dictionary_file
]
words_over_five_letters = [
word
for word in words
if len(word) > 5
]

# Store the reverse of all long words
reversed_words = set()
for word in words_over_five_letters:

reversible_words = [
word
for word in words_over_five_letters
if word in reversed_words
]

for word in reversible_words:
print(word)
```

The `word` list comprehension is only looped over once, which makes it a prime candidate for a generator expression.

```with open('dictionary.txt') as dictionary_file:
words = (
line.rstrip()
for line in dictionary_file
)
words_over_five_letters = [
word
for word in words
if len(word) > 5
]
```

Great! Our code is continuing to become more efficient and readable.

```with open('dictionary.txt') as dictionary_file:
words = (
line.rstrip()
for line in dictionary_file
)
words_over_five_letters = [
word
for word in words
if len(word) > 5
]

# Store the reverse of all long words
reversed_words = set()
for word in words_over_five_letters:

reversible_words = [
word
for word in words_over_five_letters
if word in reversed_words
]

for word in reversible_words:
print(word)
```

## Generator Review¶

List comprehensions are to lists, as generator expressions are to generators.

Remember that generators don’t work like other iterables because generators are iterators.

They can’t be indexed:

```>>> squares
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object has no attribute '__getitem__'
```

And they can’t tell us their length:

```>>> len(squares)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'generator' has no len()
```

But you can loop over generators:

```>>> for s in squares:
...     print(s)
...
1
4
9
16
```

But only once:

```>>> for s in squares:
...     print(s)
...
```

Because generators are single-use iterables.

Do you remember how to loop over generators manually?

```>>> squares = (n ** 2 for n in numbers)
>>> next(squares)
1
>>> next(squares)
4
>>> next(squares)
9
>>> next(squares)
16
>>> next(squares)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
```

So why are generator expressions called generator expressions? Why not generator comprehensions? I don’t know.

Calling them generator comprehensions is fine because people will know what you mean.

## Iteration Tools¶

Let’s learn some more built-in functions for working with iterators.

If we want to make sure everything in our list conforms to a certain rule, we can use the `all` function for that.

```>>> all(n > 1 for n in numbers)
False
>>> all(n > 0 for n in numbers)
True
```

If we want to only make sure that some of our list conforms to a certain rule, we can use the `any` function.

```>>> any(n > 2 for n in numbers)
True
>>> any(n < 1 for n in numbers)
False
```

If we want to find the smallest or largest value in a collection, we can use `min` or `max`:

```>>> min(numbers)
1
>>> max(numbers)
4
```

## Generator Expression Exercises¶

These exercises are all in the `generators.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 is_prime
```

### Primality¶

Edit the function `is_prime` so that it returns `True` if a number is prime and `False` otherwise.

Example:

```>>> from generators import is_prime
>>> is_prime(21)
False
>>> is_prime(23)
True
```

Hint

You might want to use `any` or `all` for this.

### All Together¶

Edit the function `all_together` so that it takes any number of iterables and strings them together. Try using a generator expression to do it.

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]
```

### Interleave¶

Edit the `interleave` function so that it accepts two iterables and returns a generator object with each of the given items “interleaved” (item 0 from iterable 1, then item 0 from iterable 2, then item 1 from iterable 1, and so on).

Example:

```>>> from generators import interleave
>>> list(interleave([1, 2, 3, 4], [5, 6, 7, 8]))
[1, 5, 2, 6, 3, 7, 4, 8]
>>> nums = [1, 2, 3, 4]
>>> list(interleave(nums, (n**2 for n in nums)))
[1, 1, 2, 4, 3, 9, 4, 16]
```

### Translate¶

Edit the function `translate` so that it takes a string in one language and transliterates each word into another language, returning the resulting string.

Here is an (over-simplified) example translation dictionary for translating from Spanish to English:

```>>> words = {'esta': 'is', 'la': 'the', 'en': 'in', 'gato': 'cat', 'casa': 'house', 'el': 'the'}
```

Translate a sentence using your algorithm. An example of how this function should work:

```>>> from generators import translate
>>> translate("el gato esta en la casa")
'the cat is in the house'
```

### Parse Number Ranges¶

Edit the `parse_ranges` function so that it accepts a string containing ranges of numbers and returns a generator of the actual numbers contained in the ranges. The range numbers are inclusive.

It should work like this:

```>>> from generators import parse_ranges
>>> parse_ranges('1-2,4-4,8-10')
[1, 2, 4, 8, 9, 10]
>>> parse_ranges('0-0,4-8,20-21,43-45')
[0, 4, 5, 6, 7, 8, 20, 21, 43, 44, 45]
```

### Primes Over¶

Edit the function `first_prime_over` so that it returns the first prime number over a given number.

Example:

```>>> from generators import first_prime_over
>>> first_prime_over(1000000)
1000003
```

### Anagrams¶

Edit the function `is_anagram` so that it accepts two strings and returns True if the two strings are anagrams of each other. The function should use generator expressions. Make sure your function works with mixed case.

It should work like this:

```>>> from generators import is_anagram
>>> is_anagram("tea", "eat")
True
>>> is_anagram("tea", "treat")
False
>>> is_anagram("sinks", "skin")
False
>>> is_anagram("Listen", "silent")
True
```

The function should also ignore spaces and punctuation:

```>>> is_anagram("coins kept", "in pockets")
True
>>> is_anagram("a diet", "I'd eat")
True
```