← Назад к вопросам

Как реализовать генератор без использования yield?

2.0 Middle🔥 11 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как реализовать генератор без использования yield

Генератор — это итератор, который возвращает значения один за другим, экономя память. Хотя yield — стандартный способ, существует несколько альтернативных подходов.

1. Что такое генератор

Генератор — это объект, который реализует протокол итератора (методы __iter__ и __next__):

# Классический генератор с yield
def gen_with_yield(n):
    for i in range(n):
        yield i * 2

for val in gen_with_yield(5):
    print(val)  # 0, 2, 4, 6, 8

2. Класс-итератор вместо yield

Реализуем класс, который ведёт себя как генератор:

class CountByTwo:
    """Генератор, возвращающий чётные числа"""
    def __init__(self, n):
        self.n = n
        self.current = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.n:
            raise StopIteration
        result = self.current * 2
        self.current += 1
        return result

# Использование
for val in CountByTwo(5):
    print(val)  # 0, 2, 4, 6, 8

# Или явно вызываем next()
gen = CountByTwo(3)
print(next(gen))  # 0
print(next(gen))  # 2
print(next(gen))  # 4
# next(gen)  # StopIteration

3. Использование iter() и callable

Sentinel параттерн для создания итератора из функции:

class Sequence:
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        return iter([i for i in range(self.start, self.end)])

# Или более элегантно с функцией
def create_generator(n):
    counter = 0
    def gen():
        nonlocal counter
        if counter < n:
            result = counter * 2
            counter += 1
            return result
        return None
    
    # Sentinel паттерн: iter(callable, sentinel)
    return iter(gen, None)  # Вызывает gen() пока не вернёт None

for val in create_generator(3):
    print(val)  # 0, 2, 4

4. Использование @property и состояния

class LazySquares:
    """Ленивое вычисление квадратов"""
    def __init__(self, limit):
        self.limit = limit
        self.index = 0
    
    def __iter__(self):
        self.index = 0
        return self
    
    def __next__(self):
        if self.index >= self.limit:
            raise StopIteration
        result = self.index ** 2
        self.index += 1
        return result

for square in LazySquares(4):
    print(square)  # 0, 1, 4, 9

5. Декоратор для преобразования функции в итератор

def make_generator(func):
    """Декоратор для преобразования функции в генератор"""
    def wrapper(*args, **kwargs):
        class GeneratorAdapter:
            def __init__(self):
                self.items = list(func(*args, **kwargs))
                self.index = 0
            
            def __iter__(self):
                return self
            
            def __next__(self):
                if self.index >= len(self.items):
                    raise StopIteration
                result = self.items[self.index]
                self.index += 1
                return result
        
        return GeneratorAdapter()
    
    return wrapper

@make_generator
def my_sequence(n):
    return [i * 2 for i in range(n)]

for val in my_sequence(5):
    print(val)  # 0, 2, 4, 6, 8

6. Использование itertools

Модуль itertools предоставляет готовые генераторы:

import itertools

# Бесконечный счётчик
for val in itertools.islice(itertools.count(0, 2), 5):
    print(val)  # 0, 2, 4, 6, 8

# Повтор значения
for val in itertools.islice(itertools.repeat('A'), 3):
    print(val)  # A, A, A

# Цепочка итераторов
for val in itertools.chain([1, 2], [3, 4]):
    print(val)  # 1, 2, 3, 4

# Циклическое повторение
for val in itertools.islice(itertools.cycle([1, 2, 3]), 7):
    print(val)  # 1, 2, 3, 1, 2, 3, 1

7. List comprehension как ленивой вычисления

# List comprehension (не ленивое)
result_list = [i * 2 for i in range(1000000)]  # Вся память сразу

# Generator expression (ленивое)
result_gen = (i * 2 for i in range(1000000))  # На лету

# Использование
for val in result_gen:
    print(val)
    if val > 10:
        break  # Вычислены только первые значения

8. Класс с методом call для конфигурируемого генератора

class RangeGenerator:
    """Генератор, который можно переиспользовать"""
    def __init__(self, start, stop, step=1):
        self.start = start
        self.stop = stop
        self.step = step
    
    def __iter__(self):
        current = self.start
        while current < self.stop:
            yield current  # OK, здесь мы используем yield
            current += self.step
    
    def __call__(self):
        """Сделать генератор вызываемым"""
        return iter(self)

gen = RangeGenerator(0, 10, 2)
for val in gen():  # Вызываем как функцию
    print(val)  # 0, 2, 4, 6, 8

9. Map и filter как альтернатива

# Вместо:
# def gen(n):
#     for i in range(n):
#         if i % 2 == 0:
#             yield i * 2

# Используем:
result = map(lambda x: x * 2, filter(lambda x: x % 2 == 0, range(10)))
for val in result:
    print(val)  # 0, 4, 8, 12, 16

10. Функция, возвращающая итератор

def create_fibonacci(limit):
    """Возвращает итератор Фибоначчи"""
    class FibonacciIterator:
        def __init__(self, limit):
            self.limit = limit
            self.a, self.b = 0, 1
            self.count = 0
        
        def __iter__(self):
            return self
        
        def __next__(self):
            if self.count >= self.limit:
                raise StopIteration
            result = self.a
            self.a, self.b = self.b, self.a + self.b
            self.count += 1
            return result
    
    return FibonacciIterator(limit)

for fib in create_fibonacci(7):
    print(fib)  # 0, 1, 1, 2, 3, 5, 8

11. Сравнение производительности

import timeit

# Генератор с yield
def gen_yield(n):
    for i in range(n):
        yield i

# Класс-итератор
class GenClass:
    def __init__(self, n):
        self.n = n
        self.i = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        result = self.i
        self.i += 1
        return result

# Тест
n = 1000000
print(timeit.timeit(lambda: sum(gen_yield(n)), number=10))
print(timeit.timeit(lambda: sum(GenClass(n)), number=10))
# Результат: примерно одинаковая производительность

12. Практический пример: Чтение файла построчно

# С yield
def read_lines_with_yield(filename):
    with open(filename) as f:
        for line in f:
            yield line.strip()

# Без yield
class FileLineIterator:
    def __init__(self, filename):
        self.file = open(filename)
    
    def __iter__(self):
        return self
    
    def __next__(self):
        line = self.file.readline()
        if not line:
            self.file.close()
            raise StopIteration
        return line.strip()

# Использование
for line in FileLineIterator('data.txt'):
    print(line)

Заключение

Хотя yield — это идеальный способ создать генератор, существуют альтернативы: классы-итераторы, itertools, functions как итераторы, generator expressions. Выбор зависит от задачи: для простых случаев используй yield, для сложной логики состояния — классы, для встроенных паттернов — itertools.

Как реализовать генератор без использования yield? | PrepBro