Итерируемые объекты, range, enumerate, zip, генераторные выражения, yield
Ленивость — сила: генераторы позволяют работать с бесконечными или огромными последовательностями без потребления памяти.
Два ключевых понятия:
__iter__(), возвращающим итератор. Примеры: list, str, dict, set, range.__iter__() и __next__(). __next__() возвращает следующий элемент или вызывает StopIteration.lst = [1, 2, 3] # iterable
it = iter(lst) # iterator = lst.__iter__()
print(next(it)) # 1 — вызывает it.__next__()
print(next(it)) # 2
print(next(it)) # 3
next(it) # StopIteration!Ключевое различие:
for x in lst: ... можно вызвать снова.it = iter([1, 2])
list(it) # [1, 2]
list(it) # [] — итератор уже исчерпан!class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self # итератор возвращает self
def __next__(self):
if self.n <= 0:
raise StopIteration
val = self.n
self.n -= 1
return val
for i in Countdown(3):
print(i) # 3, 2, 1
# Можно создать отдельный класс-итератор и iterable:
class NumberRange:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return NumberRangeIterator(self.start, self.end)
class NumberRangeIterator:
def __init__(self, current, end):
self.current = current
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
val = self.current
self.current += 1
return val
r = NumberRange(1, 4)
print(list(r)) # [1, 2, 3]
print(list(r)) # [1, 2, 3] — снова, потому что r — iterable, не iteratoriter() в двухаргументной формеimport io
buf = io.StringIO("line1\nline2\nSTOP\nline3\n")
# iter(callable, sentinel): вызывает callable до тех пор, пока не вернёт sentinel
for line in iter(buf.readline, "STOP\n"):
print(line.strip()) # line1, line2 — останавливается на STOPyieldФункция с yield превращается в генераторную функцию — вызов создаёт объект-генератор.
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
gen = fibonacci(5) # объект-генератор создан, код ещё не выполнялся
print(type(gen)) # <class 'generator'>
print(next(gen)) # 0 — выполняется до первого yield
print(next(gen)) # 1
print(list(gen)) # [1, 2, 3] — остатокЧто происходит при yield:
next() — продолжает с этой точкиdef gen_with_return():
yield 1
yield 2
return "done" # значение return доступно через StopIteration
g = gen_with_return()
next(g) # 1
next(g) # 2
try:
next(g)
except StopIteration as e:
print(e.value) # "done"send() — двунаправленная коммуникацияdef accumulator():
total = 0
while True:
value = yield total # yield возвращает total И получает значение через send()
if value is None:
break
total += value
gen = accumulator()
next(gen) # запускаем генератор (до первого yield)
gen.send(10) # 10
gen.send(20) # 30
gen.send(5) # 35Важно: первый вызов должен быть
next(gen)илиgen.send(None)— нельзя сразу отправить значение незапущенному генератору.
throw() и close()def safe_gen():
try:
while True:
yield
except GeneratorExit:
print("Генератор закрыт") # вызывается при gen.close()
except ValueError as e:
print(f"Получено исключение: {e}")
g = safe_gen()
next(g)
g.throw(ValueError, "ошибка!") # вбрасывает исключение в точке yield
g.close() # завершает генератор (вызывает GeneratorExit)yield from — делегированиеyield from iterable делегирует итерацию другому генератору или итерируемому.
# Без yield from:
def chain_v1(a, b):
for item in a:
yield item
for item in b:
yield item
# С yield from:
def chain_v2(a, b):
yield from a
yield from b
list(chain_v2([1, 2], [3, 4])) # [1, 2, 3, 4]Главное преимущество — поддержка send() и throw() сквозь делегирование:
def flatten(nested):
"""Рекурсивное выравнивание вложенных списков"""
for item in nested:
if isinstance(item, list):
yield from flatten(item) # рекурсивное делегирование
else:
yield item
list(flatten([1, [2, [3, 4], 5], 6])) # [1, 2, 3, 4, 5, 6]Синтаксис: (expr for item in iterable if condition) — ленивый аналог list comprehension.
squares = (x**2 for x in range(10)) # генератор, 0 памяти
print(next(squares)) # 0
print(list(squares)) # [1, 4, 9, 16, 25, 36, 49, 64, 81]
# vs
list_sq = [x**2 for x in range(10)] # список, занимает память сразу
# Использование как аргумента функции — скобки не нужны:
total = sum(x**2 for x in range(10)) # 285| Форма | Тип результата | Память | Многоразовый |
|---|---|---|---|
[x for x in ...] | list | O(n) сразу | ✅ |
(x for x in ...) | generator | O(1) | ❌ |
{x for x in ...} | set | O(n) сразу | ✅ |
{k: v for k, v in ...} | dict | O(n) сразу | ✅ |
В Python 3 многие функции возвращают итераторы (в Python 2 они возвращали списки):
m = map(str, [1, 2, 3]) # <map object>
f = filter(None, [0, 1, 2, ""]) # <filter object>
z = zip([1, 2], ['a', 'b']) # <zip object>
r = reversed([1, 2, 3]) # <list_reverseiterator>
# Нужен список — явный вызов list():
list(map(str, [1, 2, 3])) # ['1', '2', '3']| Функция | Описание |
|---|---|
range(start, stop, step) | Ленивая последовательность целых чисел |
enumerate(it, start=0) | (index, value) кортежи |
zip(*iterables) | Поэлементные кортежи (по shortest) |
map(func, *iterables) | Применяет func к каждому элементу |
filter(func, iterable) | Фильтрует элементы |
reversed(seq) | Обратный итератор (только для последовательностей) |
any() и all() — короткое замыкание# any() останавливается при первом True
any(x > 5 for x in range(100)) # True, останавливается на x=6
# all() останавливается при первом False
all(x < 10 for x in [1, 2, 100, 3]) # False, останавливается на 100
# Это делает их эффективными для ленивой проверки условийitertools — арсенал для работы с итераторамиfrom itertools import (
count, cycle, repeat, # бесконечные
chain, islice, takewhile, # конечные
dropwhile, groupby, accumulate, # конечные
product, permutations, # комбинаторные
combinations, combinations_with_replacement
)from itertools import count, cycle, repeat
count(10) # 10, 11, 12, 13, ...
count(0, 0.5) # 0, 0.5, 1.0, 1.5, ...
cycle([1, 2, 3]) # 1, 2, 3, 1, 2, 3, ...
repeat(42) # 42, 42, 42, ... (бесконечно)
repeat(42, 3) # 42, 42, 42 (ровно 3 раза)
# Использование с islice для ограничения:
from itertools import islice
list(islice(count(0), 5)) # [0, 1, 2, 3, 4]from itertools import chain, islice, takewhile, dropwhile, groupby, accumulate
# chain — объединение итераторов
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]])) # [1, 2, 3, 4]
# islice — срез итератора
list(islice([1,2,3,4,5], 3)) # [1, 2, 3]
list(islice([1,2,3,4,5], 1, 4)) # [2, 3, 4]
list(islice([1,2,3,4,5], 0, 5, 2)) # [1, 3, 5]
# takewhile — пока предикат истинен
list(takewhile(lambda x: x < 5, [1, 3, 7, 2, 4])) # [1, 3]
# dropwhile — пропускает пока предикат истинен
list(dropwhile(lambda x: x < 5, [1, 3, 7, 2, 4])) # [7, 2, 4]
# accumulate — кумулятивные операции
from itertools import accumulate
import operator
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] (кумулятивная сумма)
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] (кумулятивное произведение)
list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] (бегущий максимум)
# groupby — группировка по ключу (данные должны быть отсортированы!)
from itertools import groupby
data = sorted([("a", 1), ("b", 2), ("a", 3)], key=lambda x: x[0])
for key, group in groupby(data, key=lambda x: x[0]):
print(key, list(group))
# a [('a', 1), ('a', 3)]
# b [('b', 2)]⚠️ Ловушка
groupby: группирует только смежные элементы! Всегда сортируйте по тому же ключу перед вызовом.
from itertools import product, permutations, combinations, combinations_with_replacement
# product — декартово произведение (как вложенные циклы for)
list(product([1, 2], ['a', 'b'])) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product("AB", repeat=2)) # ('A','A'), ('A','B'), ('B','A'), ('B','B')
# permutations — перестановки
list(permutations([1, 2, 3], 2)) # (1,2), (1,3), (2,1), (2,3), (3,1), (3,2)
# combinations — комбинации без повторений
list(combinations([1, 2, 3], 2)) # (1,2), (1,3), (2,3)
# combinations_with_replacement — с повторениями
list(combinations_with_replacement([1, 2], 2)) # (1,1), (1,2), (2,2)def read_lines(filename):
"""Читаем файл строка за строкой — не загружая всё в память"""
with open(filename) as f:
yield from f
def parse_lines(lines):
"""Парсим каждую строку"""
for line in lines:
yield line.strip().split(',')
def filter_empty(rows):
"""Фильтруем пустые строки"""
for row in rows:
if any(row):
yield row
# Компоновка пайплайна:
lines = read_lines('data.csv')
rows = parse_lines(lines)
valid = filter_empty(rows)
for row in valid:
process(row)
# Вся цепочка работает лениво — каждый элемент проходит через весь пайплайн# Жадно — загружает всё в память
urls = ['...'] * 1_000_000
results = [fetch(url) for url in urls] # 1M запросов сразу!
# Лениво — генератор, запросы по мере необходимости
results = (fetch(url) for url in urls)
for result in results:
process(result) # fetch вызывается только здесьdef safe_iter(data):
"""Возвращает новый итератор при каждом вызове"""
return iter(data) # если data — iterable, это безопасно
# Антипаттерн: сохранение итератора и попытка пройти дважды
it = iter([1, 2, 3])
first_pass = list(it) # [1, 2, 3]
second_pass = list(it) # [] — итератор исчерпан!from itertools import accumulate, count, takewhile
import math
# Простые числа (решето Эратосфена через генераторы)
def primes():
"""Бесконечный генератор простых чисел"""
sieve = {}
n = 2
while True:
if n not in sieve:
yield n
sieve[n * n] = [n]
else:
for p in sieve[n]:
sieve.setdefault(p + n, []).append(p)
del sieve[n]
n += 1
first_10 = list(islice(primes(), 10)) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]import asyncio
# Асинхронный генератор
async def async_range(n):
for i in range(n):
await asyncio.sleep(0) # yield control
yield i
# Использование через async for:
async def main():
async for val in async_range(5):
print(val)
# Асинхронный итератор через класс:
class AsyncCounter:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __aiter__(self):
return self
async def __anext__(self):
if self.current >= self.stop:
raise StopAsyncIteration
await asyncio.sleep(0)
self.current += 1
return self.currentgen = (x**2 for x in range(5))
print(sum(gen)) # 30
print(sum(gen)) # 0 — уже исчерпан!
# Решение: функция-фабрика или list:
def squares(n):
return (x**2 for x in range(n))
print(sum(squares(5))) # 30
print(sum(squares(5))) # 30 — каждый раз новый генераторlst = [1, 2, 3, 4]
for item in lst:
if item % 2 == 0:
lst.remove(item) # RuntimeError или пропуск элементов!
# Решение: итерируйте по копии:
for item in lst[:]:
if item % 2 == 0:
lst.remove(item)groupby без сортировкиdata = [('a', 1), ('b', 2), ('a', 3)] # не отсортированы
for key, group in groupby(data, key=lambda x: x[0]):
print(key, list(group))
# a [('a', 1)] ← только первый 'a'!
# b [('b', 2)]
# a [('a', 3)] ← второй 'a' отдельная группа
# Правильно:
for key, group in groupby(sorted(data, key=lambda x: x[0]), key=lambda x: x[0]):
print(key, list(group))InfiniteCounter(start, step) через класс.itertools.groupby, сгруппируйте слова по первой букве.take(n, iterable) через islice.any() и all() эффективны с генераторами.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.