📦 Репозиторий занятия 56

Урок 56. Генераторы

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции115 строк

Генератор vs итератор: в чём разница

less_29__Generators/theory_01__Generator_vs_Iterator.md

Объясняет, что генератор — это частный случай итератора, и сравнивает их по пяти признакам: способ создания (класс с __iter__/__next__ вручную против yield или генераторного выражения), кто управляет состоянием, простота написания, способность принимать значения через send() и расход памяти. Приводится полный пример собственного класса-итератора Counter рядом с эквивалентной генераторной функцией counter.

  • Класс Counter с __iter__/__next__ — итератор вручную
  • def counter(n) с yield — тот же результат, но проще
  • Управление состоянием: программист (итератор) vs Python (генератор)
  • gen.send(10) — генератор умеет принимать значения, итератор нет
  • Итоговая сравнительная таблица (5 строк)
Показать начало файла (115 строк всего)
## Определение

**Генератор** — это частный случай итератора.

## Но чем они отличаются?

### 1. По способу создания

**Итератор**:

1. **Мы пока знаем ТОЛЬКО итераторы последовательностей**  
***(например, итератор списка)***

2. Чуть позже, после знакомства с ООП, мы сможем создавать итераторы со сложной логикой.

* нужно реализовать вручную два метода:

  * `__iter__()` — возвращает сам итератор
  * `__next__()` — возвращает следующее значение или `StopIteration`
* это больше кода

Пример собственного итератора:

```python
class Counter:
    def __init__(self, n):
        self.n = n
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i < self.n:
            self.i += 1
            return self.i
        raise StopIteration


c = Counter(3)

print(next(c))  # 1
print(next(c))  # 2
print(next(c))  # 3
print(next(c, 'Итератор исчерпан'))  # Итератор исчерпан

```

**Генератор**:

* создаётся автоматически:

  * либо с помощью `yield`
  * либо с помощью генераторного выражения
* не нужно писать класс и методы (т.е. не нужно описывать поведение итератора)
* программисту достаточно описать только логику появления следующего элемента.

Пример генератора:

```python
def counter(n):
    i = 0
…
Проверьте себя: Почему в примере с Counter next(c, 'Итератор исчерпан') не бросает StopIteration, хотя обычный next(c) после исчерпания бросил бы?
2
MarkdownРазбор концепции56 строк

Генераторная функция: yield вместо return

less_29__Generators/theory_02__generator_function.md

Вводит понятие генераторной функции — функции с yield, которая не возвращает всё сразу, а отдаёт значения по одному при каждом обращении. На примере countdown(n) показаны два способа потребления: вручную через next() и циклом for, с ожидаемым выводом от 5 до 1.

  • def countdown(n): while n > 0: yield n; n -= 1
  • gen = countdown(5) — вызов создаёт объект-генератор, тело ещё не выполняется
  • next(gen) x3 — получение значений по одному
  • for num in countdown(5) — потребление циклом
Показать начало файла (56 строк всего)
## Генераторная функция

**Генераторная функция** — это функция, в которой используется ключевое слово `yield`.

```python
def countdown(n):
    while n > 0:
        yield n
        n -= 1
```

Эта функция не возвращает сразу весь набор данных (как `return`),  
а **генерирует по одному значению** при каждом обращении.

---

## Создание генератор из генераторной функции

Чтобы создать генератор, нужно просто **вызвать** (инициализировать) генераторную функцию:

```python
gen = countdown(5)
```

Теперь `gen` — это объект-генератор.
Он отдаёт значения по одному.

---

## Использование генератора

### 1. Через `next()`:

```python
print(next(gen))  # 5
print(next(gen))  # 4
print(next(gen))  # 3
```

### 2. Через цикл:

```python
for num in countdown(5):
    print(num)
```

Вывод:

```
5
4
3
2
1
```
Проверьте себя: Почему вызов countdown(5) сам по себе не печатает и не вычисляет ничего, пока не будет вызван next() или не начнётся for?
3
PythonИсполняемый пример26 строк

isinstance-проверка: генераторная функция и генератор — разные вещи

less_29__Generators/theory_03__gen_vs_gen_function.py

Ключевой практический пример: сама генераторная функция countdown — это обычный Callable, а не Iterator и не Generator; только результат её вызова gen = countdown(5) становится Iterator и Generator одновременно (но не Callable). Файл явно ставит вопрос «где будет True, где False» перед проверками, а затем запускает генератор в цикле for.

  • isinstance(countdown, Iterator/Generator/Callable) — функция сама по себе
  • gen = countdown(5) — создание генератора вызовом
  • isinstance(gen, Iterator/Generator/Callable) — уже сам генератор
  • for n in gen — потребление после проверок

Что выводит: Для countdown: False, False, True (функция — Callable, не Iterator/Generator). Для gen: True, True, False. Затем for печатает 5 4 3 2 1.

Начало файла
from typing import Generator, Iterator, Callable

def countdown(n):
    while n > 0:
        yield n
        n -= 1

gen = countdown(5)

print('--- Где будет True, и где будет False? ---')

print(isinstance(countdown, Iterator))
print(isinstance(countdown, Generator))
print(isinstance(countdown, Callable))
Показать файл целиком (26 строк)
from typing import Generator, Iterator, Callable

def countdown(n):
    while n > 0:
        yield n
        n -= 1

gen = countdown(5)

print('--- Где будет True, и где будет False? ---')

print(isinstance(countdown, Iterator))
print(isinstance(countdown, Generator))
print(isinstance(countdown, Callable))

print('------------------------------------------')

print(isinstance(gen, Iterator))
print(isinstance(gen, Generator))
print(isinstance(gen, Callable))

print('=========== Запуск генератора ============')

for n in gen:
    print(n)

Проверьте себя: Почему countdown — Callable, а gen — нет, хотя gen получен именно вызовом countdown?
Открыть файл →
4
PythonИсполняемый пример19 строк

Бесконечный генератор с ручной остановкой через break

less_29__Generators/theory_04__infinite_generator.py

Генератор infinite_numbers(start, step) содержит while True с yield внутри и никогда не завершается сам — это осознанно бесконечный источник значений. Файл показывает обязательное условие безопасной работы с таким генератором: явный break внутри for по достижении условия (x >= 10), иначе цикл будет выполняться вечно.

  • def infinite_numbers(start=0, step=1) — while True: yield n; n += step
  • gen = infinite_numbers(0, 1) — создание бесконечного генератора
  • for x in gen: print(x) — потребление
  • if x >= 10: break — обязательная точка выхода

Что выводит: Печатает числа от 0 до 10 включительно, затем цикл прерывается через break.

Начало файла
# Генераторная функция
def infinite_numbers(start=0, step=1):
    n = start
    while True:
        yield n
        n += step


# Создание генератора
gen = infinite_numbers(0, 1)


# Запуск генератора
for x in gen:
Показать файл целиком (19 строк)
# Генераторная функция
def infinite_numbers(start=0, step=1):
    n = start
    while True:
        yield n
        n += step


# Создание генератора
gen = infinite_numbers(0, 1)


# Запуск генератора
for x in gen:
    print(x)   # будет выводить 0,1

    # обязательное условие для бесконечного генератора
    if x >= 10:
        break
Проверьте себя: Что произойдёт, если убрать строку `if x >= 10: break` — и почему такой генератор в принципе не может остановиться сам?
Открыть файл →
5
PythonИсполняемый пример32 строк

Метод close(): принудительная остановка генератора

less_29__Generators/theory_05__close_method.py

Показывает, как корректно остановить бесконечный генератор снаружи методом .close(), а не просто перестать его вызывать. Внутри генератора try/except GeneratorExit ловит момент закрытия и печатает сообщение; снаружи после close() следующий next() бросает StopIteration, которую перехватывает внешний while/try.

  • infinite_numbers с try/except GeneratorExit внутри тела
  • while True + next(gen) снаружи, печать n
  • gen.close() при n >= 10 — инициирует закрытие
  • except StopIteration: печать «Завершение по StopIteration» и break

Что выводит: Печатает числа 0..10, затем строку «GeneratorExit: » (перехвачено внутри генератора) и «Завершение по StopIteration» снаружи.

Начало файла
"""
.close() - метод, который:
    - прекращает работу генератора
    - и возвращает исключение StopIteration
    - в самом генераторе вызывает исключение GeneratorExit
"""

# Генераторная функция
def infinite_numbers(start=0, step=1):
    try:
        n = start
        while True:
            yield n
            n += step
Показать файл целиком (32 строк)
"""
.close() - метод, который:
    - прекращает работу генератора
    - и возвращает исключение StopIteration
    - в самом генераторе вызывает исключение GeneratorExit
"""

# Генераторная функция
def infinite_numbers(start=0, step=1):
    try:
        n = start
        while True:
            yield n
            n += step
    except GeneratorExit as e:
        print(f"{e.__class__.__name__}: {e}")


# Создание генератора
gen = infinite_numbers(0, 1)


# Закрытие генератора с помощью метода close()
while True:
    try:
        n = next(gen)
        print(n)
        if n >= 10:
            gen.close()
    except StopIteration:
        print("Завершение по StopIteration")
        break
Проверьте себя: Какое исключение получает код ВНУТРИ генератора при вызове close(), и какое исключение видит код СНАРУЖИ на следующем next()?
Открыть файл →
6
MarkdownРазбор концепции50 строк

yield from: делегирование во вложенный итерируемый объект

less_29__Generators/theory_06__yield_from.md

Объясняет конструкцию yield from — способ получить генераторную функцию из любого итерируемого объекта, допустимый только внутри тела генератора. Пример outer_gen последовательно делегирует в генераторное выражение, строку, range и вызванную генераторную функцию. Полное определение добавляет три технических момента: yield from также прозрачно прокидывает во вложенный генератор send(), исключения и завершение, а также умеет возвращать значение из return вложенного генератора.

  • yield from iterable1/iterable2/iterable3/coll_1() — делегирование по очереди
  • Важный нюанс: генераторную функцию нужно вызвать со скобками — coll_1()
  • Ожидаемая последовательность: 0,1,2,3,4, a,b,c,d,e, 0,1,2,3,4, A,B,C,D,E
  • Полное определение: проброс send(), исключений и return из вложенного генератора
Показать начало файла (50 строк всего)
 ## Конструкция `yield from`
 
### Простое (неполное) определение

`yield from` - это удобный способ получить генераторную функцию  
из любого итерируемого объекта.

И кстати, эта конструкция допустима ТОЛЬКО внутри тела генераторной функции.

```python
gen_express = (n for n in range(5))
nums_str = 'abcde'
nums_range = range(5)

def gen_f():
    for char in 'ABCDE':
        yield char


def outer_gen(iterable1, iterable2, iterable3, coll_1):
    yield from iterable1
    yield from iterable2
    yield from iterable3
    yield from coll_1()
    
    
# 0, 1, 2, 3, 4, a, b, c, d, e, 0, 1, 2, 3, 4, A, B, C, D, E, 


gen = outer_gen(gen_express, nums_str, nums_range, gen_f)

for x in gen:
    print(x, end=", ")
```

Как видим, для `yield from` годится вся, включая генераторную функцию.  
НО обратите внимание: последнюю надо добавить со скобками, чтоб получить генератор.


### Полное определение

Конструкция `yield from` позволяет:
1. последовательно получать все элементы вложенного генератора/итерируемого.
2. автоматически передавать внутрь вложенного генератора:

   * отправленные через `.send()` значения,
   * исключения,
   * завершение итерации.

3. возвращать значение из `return` внутреннего генератора (если оно есть).
Проверьте себя: Почему в вызове outer_gen последний аргумент передан как coll_1, а внутри использован как coll_1() — что было бы, если передать coll_1() сразу при вызове outer_gen?
7
MarkdownРазбор концепции33 строк

Метод send(): двустороннее общение с генератором

less_29__Generators/theory_07__send_mesthod.md

Объясняет send(value) как способ не только получать значение из генератора, но и передавать значение внутрь — оно возвращается в точке yield. На коротком примере gen() показано: первый next() запускает генератор до первого yield и возвращает 'Первое значение', а send(42) передаёт 42 в x и возвращает следующую строку с этим числом. Отдельно подчёркнуто правило: первый вызов должен быть next() или send(None), send с реальным значением до старта генератора недопустим.

  • def gen(): x = yield '...'; yield f'Получили: {x}'
  • next(g) — запуск до первого yield, вывод 'Первое значение'
  • g.send(42) — значение попадает в x, вывод 'Получили: 42'
  • Правило: первый send() не может быть с ненулевым значением
Показать начало файла (33 строк всего)
### Запущенный генератор может не только выдавать значения, но и принимать их!

1. **Что делает**:
   Метод `send(value)` позволяет не просто получать следующее значение из генератора (как `next()`),  
   а **отправить генератору значение**, которое будет возвращено в месте `yield`.

2. **Как работает**:

   ```python
   def gen():
       x = yield "Первое значение"
       yield f"Получили: {x}"

   g = gen()
   print(next(g))        # Запускаем генератор, вывод: "Первое значение"
   print(g.send(42))     # Отправляем 42, вывод: "Получили: 42"
   ```

   Здесь:

   * Первый `next()` запускает генератор до первого `yield`.
   * `send(42)` передает значение 42 в `x = yield ...`.

3. **Важно**:

   * Первый раз нельзя вызывать `send` с ненулевым значением — генератор ещё не стартовал. 
   * Для запуска нужно `next(g)` или `g.send(None)`.

4. **Полезность**:

   * Позволяет **двустороннее взаимодействие**: генератор выдает значение и одновременно принимает данные.
   * Часто используется для **корутин**, потоков данных или сложной логики генерации.
Проверьте себя: Почему нельзя вызвать g.send(42) первым действием, сразу после создания генератора g = gen()?
8
PythonИсполняемый пример26 строк

Метод throw(): «забрасывание» исключения внутрь генератора

less_29__Generators/theory_08__throw_method.py

Демонстрирует, что в генератор можно не только слать значения через send(), но и вбросить исключение методом .throw(). Исключение ValueError, брошенное снаружи, попадает в точку текущего yield внутри генератора и перехватывается его собственным try/except; при этом после обработки генератор завершается — и это проявляется как StopIteration снаружи в момент вызова throw().

  • my_generator: try/except ValueError вокруг двух yield
  • next(gen) — печатает 1
  • gen.throw(ValueError(...)) — исключение ловится ВНУТРИ генератора
  • После throw генератор завершается — снаружи это StopIteration

Что выводит: Печатает 1, затем 'ValueError: Specially thrown exception' (перехвачено внутри генератора), затем 'StopIteration: ' (генератор завершился после throw).

Начало файла
"""
В генератор можно не только передавать значения,
но даже можно "забрасывать" исключения!
"""

def my_generator():
    try:
        yield 1
        yield 2
    except ValueError as e:
        # Здесь может быть код, который запустится после получения исключения
        print(f"{e.__class__.__name__}: {e}")

Показать файл целиком (26 строк)
"""
В генератор можно не только передавать значения,
но даже можно "забрасывать" исключения!
"""

def my_generator():
    try:
        yield 1
        yield 2
    except ValueError as e:
        # Здесь может быть код, который запустится после получения исключения
        print(f"{e.__class__.__name__}: {e}")


gen = my_generator()
print(next(gen))  # Выводит 1
if next(gen) < 3:
    try:
        gen.throw(ValueError("Specially thrown exception"))
    except StopIteration as e:
        # с методом .throw генератор завершается нестандартно!
        print(f"{e.__class__.__name__}: {e}")




Проверьте себя: Почему после gen.throw(...) код снаружи получает StopIteration, хотя брошено было ValueError, а не StopIteration?
Открыть файл →
9
MarkdownРазбор концепции98 строк

Три способа найти один элемент: for, list comprehension, next()+генератор

less_29__Generators/theory_09__for__vs__list_comp__vs__next_&_generator.md

Сравнивает три подхода к поиску первого элемента списка словарей по id: обычный цикл for с return, list comprehension с последующим matches[0], и next() с генераторным выражением и значением по умолчанию None. Явно называет list comprehension плохим выбором для поиска одного элемента — он строит весь список результатов, даже если нужный элемент найден в начале, что тратит память и время. Вариант next()+генератор назван лучшим по памяти и лаконичности ценой меньшей очевидности для новичков.

  • Вариант 1: for + return — понятно, но многословно
  • Вариант 2: list comprehension + matches[0] if matches else None — лишний список, проходит весь items
  • Вариант 3: next((item for item ...), None) — не строит список, ленивый
  • Итоговое сравнение плюсов/минусов трёх подходов
Показать начало файла (98 строк всего)
**Задача**:  
Реализовать функцию, которая 
* принимает список словарей и значение ключа
* и возвращает первый элемент, удовлетворяющий условию

``` python
data = [
    {"id": 1, "name": "Leo Tolstoy"},
    {"id": 2, "name": "Fyodor Dostoevsky"},
    {"id": 3, "name": "Jane Austen"},
    {"id": 4, "name": "Mark Twain"},
    {"id": 5, "name": "George Orwell"}
  ]
```


---

## Сравним 3 подхода

Ищем **один** элемент по `id`.

---

### Вариант 1. Обычный `for` + `return`

```python
def find_by_id(items, item_id):
    for item in items:
        if item["id"] == item_id:
            return item
    return None
```

#### ✔ Плюсы

* максимально понятно
* легко дебажить
* лучший вариант для новичков

#### ❌ Минусы

* много «шумного» кода
* намерение (*«найти первый элемент»*) менее очевидно

---

### Вариант 2. `list comprehension`

```python
def find_by_id(items, item_id):
    matches = [item for item in items if item["id"] == item_id]
    return matches[0] if matches else None
```

#### ❌ Минусы (критично)

* проходит **весь список**, даже если элемент найден в начале
* создаёт **лишний список**
* больше памяти
…
Проверьте себя: Почему вариант с list comprehension хуже по производительности даже если искомый элемент — первый в списке items?