Как реализовать генератор без использования yield?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализовать генератор без использования 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.