<jupyter_start><jupyter_text>ГенераторыС точки зрения реализации, генератор в Python — это языковая конструкция, которую можно реализовать двумя способами: как функция с ключевым словом ```yield``` или как генераторное выражение. В результате вызова функции или вычисления выражения, получаем объект-генератор типа ```generator```.Пример выражения-генератора аналога ```range``` и его эквивалент в виде функции-генератора:<jupyter_code>g = (i for i in range(5))
print(f'{g = }')
print(f'{type(g) = }')
print(f'{dir(g) = }')
print(f'{hasattr(g, "__iter__") = }')
print(f'{hasattr(g, "__next__") = }')
def count(n):
    for i in range(n):
        yield i

g = count(5)
print(f'{g = }')
print(f'{type(g) = }')<jupyter_output>g = <generator object count at 0x000001AD343E5200>
type(g) = <class 'generator'><jupyter_text>В объекте-генераторе определены методы ```__next__``` и ```__iter__```, то есть, реализован протокол итератора, с этой точки зрения, в Python любой генератор является итератором.Концептуально, итератор -- это механизм поэлементного обхода данных, а генератор позволяет отложено (лениво) создавать результат при итерации. Генератор может создавать результат на основе какого-то алгоритма или брать элементы из источника данных (коллекция, файлы, сетевое подключения и т.д.) и изменять их.Примером генератора является функция ```enumerate```:<jupyter_code>res = enumerate([1, 2, 3])
print(dir(res))<jupyter_output>['__class__', '__class_getitem__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']<jupyter_text>Генераторы могут быть конечны и бесконечны. Функция ```count```, описанная выше, конечный генератор. Заменяя в ней цикл с условием на бесконечный цикл можно получить бесконечный счетчик.Данная функция уже реализована в модуле ```itertools```.Рассмотрим работу генераторов более подробно на следующей функции.<jupyter_code>def foo():
    print('--> 1')
    yield 1
    print('--> 2')
    yield 2<jupyter_output><empty_output><jupyter_text>Инструкция ```yield``` временно приостанавливает работу функции-генератора, а не завершает ее как ```return``` или появление исключения. Для пошаговой демонстрации воспользуемся функцией ```next```. Вызывая функцию ```next```, генератор ```foo``` выполняется до соответствующей инструкции ```yield```. При первом вызове ```next```, выполнение дойдет до строки ```yield 1``` и выполнение остановиться, в ожидании следующего вызова ```next```. Выражение, стоящее справа от ```yield``` будет возвращено как новое значение генератора. Справа от ```yield``` может ничего не быть, в этом случае будет возвращено ```None```.Когда генератор будет исчерпан, например, выполнение дойдет до инструкции ```return```, то автоматически будет возвращено исключение ```StopIteration```. Вся информация, которая будет указана после ```return```, добавиться в описание исключения. Стоит помнить, что в Python функции возвращают ```None``` даже в условии отсутствия ```return```.<jupyter_code>gen = foo()
print(f'{gen = }')
print(f'{type(gen) = }')
print('-' * 30)

print(f'{next(gen) = }')
print(f'{next(gen) = }')
print(f'{next(gen) = }')  # StopIteration<jupyter_output>gen = <generator object foo at 0x000001AD347C2200>
type(gen) = <class 'generator'>
------------------------------
--> 1
next(gen) = 1
--> 2
next(gen) = 2<jupyter_text>Попробуйте пошагово выполнить этот код на [pythontutor](http://www.pythontutor.com/visualize.htmlcode=def%20foo%28%29%3A%0A%20%20%20%20print%28'--%3E%201'%29%0A%20%20%20%20yield%201%0A%20%20%20%20print%28'--%3E%202'%29%0A%20%20%20%20yield%202%0A%0Agen%20%3D%20foo%28%29%0Aprint%28next%28gen%29%29%0Aprint%28next%28gen%29%29&cumulative=false&curInstr=0&heapPrimitives=true&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) и проследить, как работает генератор.Генераторы, как и итераторы, исчерпаемы, это значит повторное использование невозможно.<jupyter_code>def foo():
    yield 1
    yield 2

gen = foo()

print(f'(1): {list(gen) = }')
print(f'(2): {list(gen) = }')<jupyter_output>(1): list(gen) = [1, 2]
(2): list(gen) = []<jupyter_text>Ниже приведен пример полноценного генераторы, вычисляющего новые значения по определенному алгоритму.Функция ```kaprekar_function``` вычисляет количество шагов выполнения [функции Капрекара](https://en.wikipedia.org/wiki/Kaprekar%27s_routine), пока число не достигнет определенной константы. Генератор ```gkaprekar``` проверяет первые ```n``` чисел.<jupyter_code>from typing import Union, Generator


def kaprekar_function(n: int) -> tuple[int, int]:
    """Функция Капрекара.
    Совершает действие: вычислить разницу между числом, 
    состоящим из цифр исходного числа, стоящих по убыванию, 
    и числом, состоящим из цифр исходного числа, стоящих 
    по возрастанию. 
    Шаги прекращаются когда число совпадает 
    с постоянной Капрекара.
    Подробнее см.: https://en.wikipedia.org/wiki/Kaprekar%27s_routine

    >>> kaprekar_function(876)
    (495, 5)
    >>> kaprekar_function(3412)
    (6174, 3)
    
    :param n: исходное число
    :type n: int
    :return: пару чисел (x, y), где x - константа к которой 
             сходится исходное число, y - число шагов.
    :rtype: tuple[int, int]
    """
    count = 0
    old_n = 0
    while n != old_n and n > 0:
        old_n = n
        digits = list(str(n))
        n1 = int(''.join(sorted(digits, reverse=True)))
        n2 = int(''.join(sorted(digits)))
        n = n1 - n2
        count += 1
    return old_n, count - 1 if count > 1 else count

def gkaprekar(start: int, stop: Union[int]=None, /) -> Generator[tuple[int, tuple[int, int]], None, None]:
    """Генератор для функции Капрекара.
    Подробнее см. kaprekar_function.

    >>> list(gkaprekar(5))
    [(0, (0, 0)), (1, (0, 1)), (2, (0, 1)), (3, (0, 1)), (4, (0, 1))]
    >>> list(gkaprekar(10, 15))
    [(10, (0, 1)), (11, (0, 1)), (12, (0, 1)), (13, (0, 5)), (14, (0, 3))]

    :param start: Когда функция вызывается с одним параметрам 
                  этот аргумент означает правую границу 
                  интервала [0, start). В случае вызова функции 
                  с двумя аргументами, он характеризует левую 
                  границу [start, stop).
    :type start: int
    :param stop: В случае вызова функции с двумя аргументами, stop 
                 характеризует правую границу [start, stop).
    :type stap: int

    :return: Пары выда (i, K(i)), где i - проверяемое число, 
             K(i) - значение функции Капрекара (см. kaprekar_function).
    :rtype: Generator[tuple[int, tuple[int, int]], None, None]
    """
    if stop is None:
        start, stop = 0, start
    for i in range(start, stop):
        yield i, kaprekar_function(i)

for j, item in gkaprekar(10, 15):
    print(j, item)<jupyter_output>10 (9, 1)
11 (11, 1)
12 (9, 1)
13 (9, 5)
14 (9, 3)<jupyter_text>Параметрические генераторыОбычные генераторы великолепны, но что делать если в процессе работы генератора ему нужно передавать какие-либо параметры? Эту задачу решает инструкция ```yield``` и метод ```send```. Рассмотренные до этого генераторы использовали ```yield``` только для возвращения нового значения. Но используя ```yield``` также можно обозначить место, куда можно передать значение. В следующем примере демонстрируется эта возможность. Теперь в генераторе есть строка ```x = yield 1```. Она обозначает, что то, что стоит после ```yield``` будет возвращено из генератора (как в предыдущих примерах), но теперь мы связываем результат, которые возвращает ```yield``` с именем ```x```.<jupyter_code>def foo():
    x = yield 1
    yield x

gen = foo()
print(f'{next(gen) = }')
print(f'{next(gen) = }')<jupyter_output>next(gen) = 1
next(gen) = None<jupyter_text>Постойте, мы вызываем этот генератор два раза. В первом случае, как и ожидалось, возвращается 1, а почему во втором случае вернулось ```None```? И каким образом можно передать значение в генератор? Дело в том, что в процессе работы генератору нельзя передать значения "классическим" способом, с помощью круглых скобок. Для этого используется специальный метод ```send```. Этот метод принимает значения, они передаются в соответствующее место внутри генератора.<jupyter_code>gen = foo()
print(f'{next(gen) = }')
print(f'{gen.send(3) = }')<jupyter_output>next(gen) = 1
gen.send(3) = 3
{'fraction_non_alphanumeric': 0.14255444379046497, 'fraction_numerical': 0.03001765744555621, 'mean_word_length': 4.736664415935179, 'pattern_counts': {'":': 0, '<': 30, '<?xml version=': 0, '>': 50, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '649185', 'license': 'MIT', 'n_tokens_mistral': 3557, 'n_tokens_neox': 3883, 'n_words': 859, 'path': 'python_pd/04_functions/08_generators.ipynb', 'repo_name': 'Alexey-smurf/python-bp', 'size': '8495'}