# Generators #

## What is a generator? #

**Generators** can be used to create your own iterators with custom values. This is useful when you want to create an iterator with unpredictable results (for example numbers with irregular intervals, possibly based on different arguments in a given function).

The way you define a generator function is through the `yield`

keyword (instead of `return`

in a function). In this sense, a generator is a type of iterator that **yields** results from a **generator function**.

```
def numbers():
num = 0
while num < 3:
yield num
num += 1
```

Calling the generator function will return a generator (which itself is an iterator, which means the `next`

function works similarly to how it does for iterators):

```
num_iter = numbers()
next(num_iter) # 0
next(num_iter) # 1
next(num_iter) # 2
next(num_iter) # StopIterator
```

## How generators work #

When the function called, Python **immediately** returns an iterator without running any lines of code in the function itself.

When `next`

is called on the iterator, it starts executing the body of the function until it hits the first `yield`

statement. Once it hits the `yield`

statement, the function **pauses** execution (not to be confused with `return`

where the function completely stops executing), and when `next`

is called again, the function will continue executing from where it paused. If it doesn’t find any `yield`

statements from there, it raises a `StopIteration`

exception.

```
def printer(n):
print("we have started")
while n < 3:
yield n
n = n + 1
printing = printer(0)
next(printing)
# we have started
# 0
next(printing)
# 1
next(printing)
# 2
next(printing)
# StopIteration
```

We can use for loops over generators like we can over iterators:

```
def printer(n):
print("we have started")
while n < 3:
yield n
n = n + 1
for item in printer(0):
print(item)
# we have started (goes through the function body as normal)
# 0
# 1
# 2
```

## Why use generators? #

For some functions, using generators can save a lot of computing time. This is because they only generate the next item **when needed** (instead of having to find every solution in a recursive solution for example, you can just compute one element as a time (as you need it) in certain situations).

### Example: Fibonacci Numbers #

Instead of an iterative solution

```
def fib(n):
prev = 0
curr = 1
iterations = 1
while iterations < n:
prev, curr = curr, prev + curr
iterations += 1
return curr
```

we can use a generator for this:

```
def gen_fib():
prev = 0
curr = 1
while True:
yield prev
prev, curr = curr, prev + curr
```

This will allow you to generate Fibonacci numbers as needed.

## Yield From #

The `yield from`

keyword allows you to yield from an iterable/iterator one at a time:

```
def lst_yielder():
lst = [0, 1, 2, 3]
yield from lst
lst = lst_yielder()
next(lst) # 0
next(lst) # 1
next(lst) # 2
next(lst) # 3
```

However, one of the very cool things about `yield from`

is that you can yield the results of another generator function, which makes it act sort of like recursion in a way.

```
def countdown(n):
if k > 0:
yield n
yield from countdown(n - 1)
```

When the function hits `yield from countdown(n - 1)`

, it makes another instance of `countdown`

, with a lower value of `n`

, which will then pause when it reaches `yield n`

in the function body.

## Generator Functions with Returns #

Using a `return`

keyword in a generator function does not work exactly as intended.

Remember how earlier it was mentioned that if a function was unable to find another `yield`

statement, it would cause a `StopIteration`

error? Well, what a `return`

statement does traditionally is exit out of a function, which in this case means that no further `yield`

statement will be found. As a result, a `StopIteration`

error will occur.

```
def f(x):
yield x
yield x + 1
return
yield x + 2
list(f(1)) # [1, 2]
```

### With Return Values #

```
def f(x):
yield x
yield x + 1
return x + 1
yield x + 2
list(f(1)) # [1, 2]
```

Notice how nothing changed? Return values can’t be yielded in this way. There is a way to yield these return values, but it is not in the scope of CS61A.

## Count Partitions (Yield) #

```
def partitions(n, m):
"""List partitions.
>>> for p in partitions(6, 4): print(p)
4 + 2
4 + 1 + 1
3 + 3
3 + 2 + 1
3 + 1 + 1 + 1
2 + 2 + 2
2 + 2 + 1 + 1
2 + 1 + 1 + 1 + 1
1 + 1 + 1 + 1 + 1 + 1
"""
if n < 0 or m == 0:
return
else:
if n == m:
yield str(m)
for p in partitions(n - m, m):
yield f"{m} + {p}"
yield from partitions(n, m - 1)
```

This one is a bit difficult to understand. Let’s break it down.

The base case `n < 0 or m == 0`

has a `return`

statement in it. What this means is that if we ever get to the point where either of these cases happen, a `StopIteration`

error will occur, which in this program, means that specific combination will not be yielded.

In the case where `n == m`

, it means that there will only be one possible combination to make up that partition, so that specific partition size will be yielded.

Afterwards, we use a for loop to deal with the case where we do use `m`

(because using `yield from`

does not allow you to change the values that you want to yield), then just yield from the case where `m`

isn’t used and it decreases by 1.