|
1 | | -#TODO: Add about for this concept. |
| 1 | +# About |
2 | 2 |
|
| 3 | +## Constructing a generator |
| 4 | + |
| 5 | +Generators are constructed much like other looping or recursive functions, but require a [`yield` expression](#the-yield-expression), which we will explore in depth a bit later. |
| 6 | + |
| 7 | + |
| 8 | +An example is a function that returns the _squares_ from a given list of numbers. |
| 9 | +As currently written, all input must be processed before any values can be returned: |
| 10 | + |
| 11 | + |
| 12 | +```python |
| 13 | +>>> def squares(list_of_numbers): |
| 14 | +>>> squares = [] |
| 15 | +>>> for number in list_of_numbers: |
| 16 | +>>> squares.append(number ** 2) |
| 17 | +>>> return squares |
| 18 | +``` |
| 19 | + |
| 20 | +You can convert that function into a generator like this: |
| 21 | + |
| 22 | +```python |
| 23 | +def squares(list_of_numbers): |
| 24 | + for number in list_of_number: |
| 25 | + yield number ** 2 |
| 26 | +``` |
| 27 | + |
| 28 | +The rationale behind this is that you use a generator when you do not need all the values _at once_. |
| 29 | + |
| 30 | +This saves memory and processing power, since only the value you are _currently working on_ is calculated. |
| 31 | + |
| 32 | + |
| 33 | +## Using a generator |
| 34 | + |
| 35 | +Generators may be used in place of most `iterables` in Python. This includes _functions_ or _objects_ that require an `iterable`/`iterator` as an argument. |
| 36 | + |
| 37 | +To use the `squares()` generator: |
| 38 | + |
| 39 | +```python |
| 40 | +>>> squared_numbers = squares([1, 2, 3, 4]) |
| 41 | + |
| 42 | +>>> for square in squared_numbers: |
| 43 | +>>> print(square) |
| 44 | +1 |
| 45 | +4 |
| 46 | +9 |
| 47 | +16 |
| 48 | +``` |
| 49 | + |
| 50 | +Values within a generator can also be produced/accessed via the `next()` function. |
| 51 | +`next()` calls the `__next__()` method of a generator object, "advancing" or evaluating the generator code up to its `yield` expression, which then "yields" or returns the value. |
| 52 | + |
| 53 | +```python |
| 54 | +square_generator = squares([1, 2]) |
| 55 | + |
| 56 | +>>> next(square_generator) |
| 57 | +1 |
| 58 | +>>> next(square_generator) |
| 59 | +4 |
| 60 | +``` |
| 61 | + |
| 62 | +When a `generator` is fully consumed and has no more values to return, it throws a `StopIteration` error. |
| 63 | + |
| 64 | +```python |
| 65 | +>>> next(square_generator) |
| 66 | +Traceback (most recent call last): |
| 67 | + File "<stdin>", line 1, in <module> |
| 68 | +StopIteration |
| 69 | +``` |
| 70 | + |
| 71 | +### Difference between iterables and generators |
| 72 | + |
| 73 | +Generators are a special sub-set of _iterators_. |
| 74 | +`Iterators` are the mechanism/protocol that enables looping over _iterables_. |
| 75 | +Generators and and the iterators returned by common Python (`iterables`)[https://wiki.python.org/moin/Iterator] act very similarly, but there are some important differences to note: |
| 76 | + |
| 77 | + |
| 78 | +- Generators are _one-way_; there is no "backing up" to a previous value. |
| 79 | + |
| 80 | +- Iterating over generators consume the returned values; no resetting. |
| 81 | +- Generators (_being lazily evaluated_) are not sortable and can not be reversed. |
| 82 | + |
| 83 | +- Generators do _not_ have `indexes`, so you can't reference a previous or future value using addition or subtraction. |
| 84 | + |
| 85 | +- Generators cannot be used with the `len()` function. |
| 86 | + |
| 87 | +- Generators can be _finite_ or _infinite_, be careful when collecting all values from an _infinite_ generator. |
| 88 | + |
| 89 | +## The yield expression |
| 90 | + |
| 91 | +The [yield expression](https://docs.python.org/3.8/reference/expressions.html#yield-expressions) is very similar to the `return` expression. |
| 92 | + |
| 93 | +_Unlike_ the `return` expression, `yield` gives up values to the caller at a _specific point_, suspending evaluation/return of any additional values until they are requested. |
| 94 | + |
| 95 | +When `yield` is evaluated, it pauses the execution of the enclosing function and returns any values of the function _at that point in time_. |
| 96 | + |
| 97 | +The function then _stays in scope_, and when `__next__()` is called, execution resumes until `yield` is encountered again. |
| 98 | + |
| 99 | +Note: _Using `yield` expressions is prohibited outside of functions._ |
| 100 | + |
| 101 | +```python |
| 102 | +>>> def infinite_sequence(): |
| 103 | +>>> current_number = 0 |
| 104 | +>>> while True: |
| 105 | +>>> yield current_number |
| 106 | +>>> current_number += 1 |
| 107 | + |
| 108 | +>>> lets_try = infinite_sequence() |
| 109 | +>>> lets_try.__next__() |
| 110 | +0 |
| 111 | +>>> lets_try.__next__() |
| 112 | +1 |
| 113 | +``` |
| 114 | + |
| 115 | +## Why generators? |
| 116 | + |
| 117 | +Generators are useful in a lot of applications. |
| 118 | + |
| 119 | +When working with a large collection, you might not want to put all of its values into `memory`. |
| 120 | +A generator can be used to work on larger data piece-by-piece, saving memory and improving performance. |
| 121 | + |
| 122 | +Generators are also very helpful when a process or calculation is _complex_, _expensive_, or _infinite_: |
| 123 | + |
| 124 | +```python |
| 125 | +>>> def infinite_sequence(): |
| 126 | +>>> current_number = 0 |
| 127 | +>>> while True: |
| 128 | +>>> yield current_number |
| 129 | +>>> current_number += 1 |
| 130 | +``` |
| 131 | + |
| 132 | +Now whenever `__next__()` is called on the `infinite_sequence` object, it will return the _previous number_ + 1. |
0 commit comments