# Генераторы

Для начала нам стоит познакомиться с итераторами. Как подсказывает Wiki, итератор — это интерфейс, предоставляющий доступ к элементам коллекции (массива или контейнера). Здесь важно отметить, что итератор только предоставляет доступ, но не выполняет итерацию по ним. Это может звучать довольно запутано, так что остановимся чуть подробнее. Тему итераторов можно разбить на три части:

* Итерируемый объект
* Итератор
* Итерация

Все эти три части связаны друг с другом. Мы обсудим их одну за одной и после перейдем к генераторам.

## Итерируемый объект

**Итерируемым объектом** в Python называется любой объект, имеющий методы `__iter__` или `__getitem__`, которые возвращают **итераторы** или могут принимать индексы (подробности [здесь](https://stackoverflow.com/questions/20551042/whats-the-difference-between-iter-and-getitem/20551346#20551346)). В итоге итерируемый объект это объект, который может предоставить нам **итератор**. Так что же представляет из себя **итератор**?

## Итератор

Итератором в Python называется объект, который имеет метод `next` (Python 2) или `__next__`. Вот и все. Это итератор. Теперь об **итерации**.

## Итерация

Если коротко - это процесс получения элементов из какого-нибудь источника, например списка. Итерация - это процесс перебора элементов объекта в цикле. Теперь, когда у нас есть общее понимание основных принципов, перейдём к **генераторам**.

## Генераторы

Генераторы это итераторы, по которым можно итерировать только один раз. Так происходит поскольку они не хранят все свои значения в памяти, а генерируют элементы "на лету". Генераторы можно использовать с циклом `for` или любой другой функцией или конструкцией, которые позволяют итерировать по объекту. В большинстве случаев **генераторы** создаются как функции. Тем не менее, они не возвращают значение также как функции (т.е. через `return`), в генераторах для этого используется ключевое слово `yield`. Вот простой пример функции-генератора:

```python
def generator_function():
    for i in range(10):
        yield i

for item in generator_function():
    print(item)

# Вывод: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
```

Не самый полезный код, однако достаточно наглядный. Генераторы хорошо подходят для расчета больших наборов результатов (при использовании вложенных циклов), где вам бы не хотелось выделять память для хранения всех результатов одновременно. Многие функции из стандартной библиотеки, возвращающие `списки` в Python 2, были модифицированы, для того, чтобы возвращать `генераторы` в Python 3, поскольку последние требуют меньше ресурсов.

Вот пример `генератора`, который считает числа Фибоначчи:

```python
# generator version
def fibon(n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b
```

А вот так мы можем его использовать:

```python
for x in fibon(1000000):
    print(x)
```

С помощью такого метода мы можем не волноваться об использовании большого объема ресурсов. В то же время следующая реализация алгоритма:

```python
def fibon(n):
    a = b = 1
    result = []
    for i in range(n):
        result.append(a)
        a, b = b, a + b
    return result
```

будет использовать огромный объем наших ресурсов при расчете достаточно больших чисел. Я уже говорил, что мы можем итерировать по **генераторам** только один раз, но давайте проверим это на практике. Перед этим вам надо познакомиться с одной встроенной в язык функцией - `next()`. Она позволяет нам переходить к следующему элементу коллекции. Давайте проверим наше понимание:

```python
def generator_function():
    for i in range(3):
        yield i

gen = generator_function()
print(next(gen))
# Вывод: 0
print(next(gen))
# Вывод: 1
print(next(gen))
# Вывод: 2
print(next(gen))
# Вывод: Traceback (most recent call last):
#          File "<stdin>", line 1, in <module>
#        StopIteration
```

Как видно, после прохождения по всем значениям `next()` начала вызывать исключение `StopIteration`. По сути, эта ошибка информирует нас о том, что все значения коллекции уже были пройдены. Может возникнуть вопрос, почему мы не получаем ошибку при использовании цикла `for`. И ответ довольно прост. Цикл `for` автоматически перехватывает данное исключение и перестает вызывать `next`. Знали ли вы, что несколько встроенных типов данных в Python поддерживают итерирование? Давайте посмотрим:

```python
my_string = "Yasoob"
next(my_string)
# Вывод: Traceback (most recent call last):
#          File "<stdin>", line 1, in <module>
#        TypeError: str object is not an iterator
```

Ок, это не то что ожидалось. Ошибка говорит, что `str` не итератор. И это действительно так! Строка - итерируемый объект, но не итератор. Т.е. она поддерживает итерирование, но мы не можем делать это напрямую. Так как же нам в конечном итоге итерировать по строке? Пришло время для очередной встроенной функции - `iter`. Она возвращает **итератор** из **итерируемого** объекта. `int` не является итерируемым объектом, однако мы можем использовать `iter` со строками!

```python
int_var = 1779
iter(int_var)
# Вывод: Traceback (most recent call last):
#          File "<stdin>", line 1, in <module>
#        TypeError: 'int' object is not iterable
# int не итерируемый объект

my_string = "Yasoob"
my_iter = iter(my_string)
print(next(my_iter))
# Вывод: 'Y'
```

Теперь намного лучше. Я уверен, что вас заинтересовала эта тема. Помните, что полностью изучить генераторы можно только через постоянную практику. Просто используйте **генераторы** везде, где это кажется удачным решением. Вы не разочаруетесь!
