Бесконечный счётчик и пагинация
Сегодня — чистая практика. Мы напишем бесконечный генератор, научимся его укрощать с помощью itertools.islice, а затем создадим полноценный класс Paginator, который разбивает любые данные на страницы. Эти паттерны встречаются в реальных проектах постоянно.
🎯 Постановка задачи: зачем нам эти паттерны?
Представьте два типичных сценария из реальной разработки:
🔢
Генерация уникальных ID
Система создаёт пользователей, заказы, события — каждому нужен уникальный номер. Количество заранее неизвестно.
📄
Постраничный вывод
API возвращает 10 000 товаров. Показать всё сразу — невозможно. Нужны страницы по 20 штук.
🎲
Симуляции и стримы
Игровой движок генерирует события бесконечно. Датчики шлют данные непрерывно. Остановка — внешняя.
Для первого и третьего — бесконечный генератор. Для второго — класс Paginator. Поехали!
♾️ Шаг 1 — Бесконечный счётчик
Обычная функция всегда завершается. Но генератор с while True работает вечно — он будет отдавать значения, пока вы их запрашиваете. Поскольку генератор ленивый, бесконечный цикл внутри него не «зависает» программу — он просто ждёт следующего вызова next().
def counter(start=0, step=1):
"""
Бесконечный счётчик.
start — начальное значение (по умолчанию 0)
step — шаг (по умолчанию 1)
"""
current = start
while True: # Бесконечный цикл — но это ОК для генератора!
yield current # Отдаём текущее значение и ЗАСЫПАЕМ
current += step # Просыпаемся только когда попросят следующее
Почему while True безопасен в генераторе? Каждый раз, когда выполнение доходит до yield, функция замораживается — она не занимает процессор, не жрёт память. Следующая итерация цикла начнётся только после явного запроса значения через next() или for.
Проверяем работу счётчика:
gen = counter(start=1, step=2) # нечётные числа: 1, 3, 5, 7, ...
print(next(gen)) # 1
print(next(gen)) # 3
print(next(gen)) # 5
# Можно передавать параметры
id_gen = counter(start=1000) # ID пользователей начиная с 1000
print(next(id_gen)) # 1000
print(next(id_gen)) # 1001
print(next(id_gen)) # 1002
Но что если нам нужно взять только первые N значений? Просто написать for x in counter() — бесконечный цикл! Здесь на помощь приходит itertools.islice.
✂️ Шаг 2 — itertools.islice: ограничиваем бесконечность
itertools.islice(iterable, stop) работает как срез списка, но для любого итерируемого объекта — в том числе для бесконечного генератора. Он берёт первые stop элементов и останавливается, не запрашивая остальные.
from itertools import islice
def counter(start=0, step=1):
current = start
while True:
yield current
current += step
# Берём первые 5 значений из бесконечного счётчика
first_five = list(islice(counter(1), 5))
print(first_five) # [1, 2, 3, 4, 5]
# Первые 10 чётных чисел
evens = list(islice(counter(0, step=2), 10))
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# islice с тремя аргументами: islice(iterable, start, stop)
# Пропускаем первые 3, берём следующие 5
some = list(islice(counter(100), 3, 8))
print(some) # [103, 104, 105, 106, 107]
💡 islice — это тоже генератор. Он не создаёт список сразу. Только когда вы вызываете list(), for или другую функцию-потребитель — начинается реальная работа. До этого момента ничего не вычисляется.
Это значит: islice(counter(), 1_000_000) не занимает памяти до тех пор, пока вы не начнёте итерацию!
Реальный пример — генерация уникальных ID для объектов:
from itertools import islice
def id_generator(prefix="user", start=1):
"""Генерирует ID вида user_1, user_2, ..."""
n = start
while True:
yield f"{prefix}_{n}"
n += 1
# Создаём 5 пользователей
user_ids = list(islice(id_generator("user"), 5))
print(user_ids)
# ['user_1', 'user_2', 'user_3', 'user_4', 'user_5']
# Создаём 3 заказа
order_ids = list(islice(id_generator("order", start=1000), 3))
print(order_ids)
# ['order_1000', 'order_1001', 'order_1002']
📦 Шаг 3 — Проектируем класс Paginator
Пагинация — разбивка данных на «страницы» фиксированного размера. Задача встречается везде: интернет-магазины, форумы, API-ответы. Реализуем её как класс, который умеет итерироваться — то есть его можно использовать прямо в цикле for.
Что нам нужно от класса:
- Принимает список данных и размер страницы
- Свойство
total_pages— сколько страниц всего - Метод
__iter__— чтобы писатьfor page in paginator - Каждая итерация возвращает срез данных — одну «страницу»
class Paginator:
def __init__(self, data, page_size):
"""
data — список (или любая последовательность) данных
page_size — сколько элементов на одной странице
"""
self.data = list(data) # сохраняем данные как список
self.page_size = page_size
@property
def total_pages(self):
"""Вычисляем количество страниц через целочисленное деление с округлением вверх."""
# Трюк: (N + size - 1) // size === math.ceil(N / size)
# Например, 10 элементов, по 3: (10 + 2) // 3 = 4 страницы ✅
return (len(self.data) + self.page_size - 1) // self.page_size
def __iter__(self):
"""
Возвращает итератор по страницам.
Каждая страница — это срез self.data.
Используем yield, чтобы не хранить все страницы сразу.
"""
for page_num in range(self.total_pages):
start = page_num * self.page_size
end = start + self.page_size
yield self.data[start:end] # срез: от start до end
Почему @property для total_pages? Свойство (@property) позволяет обращаться к нему как к атрибуту: p.total_pages — без скобок. При этом значение вычисляется каждый раз заново, что правильно, если данные могут меняться. Хранить его как обычный атрибут было бы менее гибко.
Используем класс:
products = list(range(1, 23)) # 22 товара: [1, 2, ..., 22]
paginator = Paginator(products, page_size=5)
print(f"Товаров: {len(paginator.data)}") # Товаров: 22
print(f"Страниц: {paginator.total_pages}") # Страниц: 5
for page_num, page in enumerate(paginator, start=1):
print(f"Страница {page_num}: {page}")
# Страница 1: [1, 2, 3, 4, 5]
# Страница 2: [6, 7, 8, 9, 10]
# Страница 3: [11, 12, 13, 14, 15]
# Страница 4: [16, 17, 18, 19, 20]
# Страница 5: [21, 22] ← последняя страница может быть неполной
Можно также обратиться к конкретной странице напрямую, добавив метод get_page:
class Paginator:
def __init__(self, data, page_size):
self.data = list(data)
self.page_size = page_size
@property
def total_pages(self):
return (len(self.data) + self.page_size - 1) // self.page_size
def __iter__(self):
for page_num in range(self.total_pages):
start = page_num * self.page_size
yield self.data[start:start + self.page_size]
def get_page(self, page_num):
"""
Возвращает страницу по номеру (нумерация с 1).
Если номер за пределами — возвращает пустой список.
"""
if page_num < 1 or page_num > self.total_pages:
return []
start = (page_num - 1) * self.page_size
return self.data[start:start + self.page_size]
# Получаем 3-ю страницу напрямую
p = Paginator(range(1, 23), page_size=5)
print(p.get_page(3)) # [11, 12, 13, 14, 15]
print(p.get_page(99)) # []
🔍 Шаг 4 — Пагинация с фильтрацией через генератор
В реальности данные нужно не только разбивать на страницы, но и фильтровать перед этим. Например, показывать только товары в наличии, только активных пользователей, только записи за этот месяц.
Реализуем функцию-генератор filtered_paginator, которая сначала фильтрует данные, а потом разбивает их на страницы. Мы воспользуемся ленивостью генераторов: фильтрация произойдёт на лету, без создания промежуточного списка.
def filtered_paginator(data, page_size, predicate):
"""
Генератор страниц с предварительной фильтрацией.
data — исходные данные (любой итерируемый объект)
page_size — размер страницы
predicate — функция-фильтр: принимает элемент, возвращает True/False
"""
page = [] # текущая страница накапливается здесь
for item in data:
if predicate(item): # проверяем условие
page.append(item)
if len(page) == page_size: # страница заполнена
yield page
page = [] # сбрасываем для следующей страницы
if page: # отдаём последнюю неполную страницу
yield page
Почему это эффективно? Мы не создаём список «все отфильтрованные элементы», а затем разбиваем его. Мы обрабатываем каждый элемент один раз: либо добавляем на текущую страницу, либо пропускаем. Страница отдаётся сразу, как только накопилась — и память освобождается.
Проверяем с реальным примером — товары с ценой выше 500 рублей:
# Каталог товаров: (название, цена)
catalog = [
("Ноутбук", 45000),
("Мышь", 350),
("Монитор", 12000),
("Клавиатура", 800),
("Веб-камера", 2500),
("Коврик", 150),
("Наушники", 3200),
("USB-хаб", 600),
("Принтер", 8500),
("Сканер", 4200),
]
# Фильтр: только товары дороже 500 рублей
expensive = lambda item: item[1] > 500
# Страницы по 3 товара
for num, page in enumerate(filtered_paginator(catalog, 3, expensive), start=1):
print(f"--- Страница {num} ---")
for name, price in page:
print(f" {name}: {price} руб.")
# --- Страница 1 ---
# Ноутбук: 45000 руб.
# Монитор: 12000 руб.
# Клавиатура: 800 руб.
# --- Страница 2 ---
# Веб-камера: 2500 руб.
# Наушники: 3200 руб.
# USB-хаб: 600 руб.
# --- Страница 3 ---
# Принтер: 8500 руб.
# Сканер: 4200 руб.
Мышь (350 руб.) и Коврик (150 руб.) отфильтрованы — их нет ни на одной странице. Всё остальное аккуратно разбито по страницам.
⚠️ Важный момент с if page: после цикла может остаться накопленная страница, которая не достигла page_size. Это последняя, неполная страница. Без if page: yield page в конце — последние элементы будут потеряны!
📋 Шаг 5 — Финальный код: всё вместе
Собираем все части в один модуль — так, как это могло бы выглядеть в реальном проекте:
from itertools import islice
# ─── 1. Бесконечный счётчик ──────────────────────────────────────────────────
def counter(start=0, step=1):
"""Бесконечный генератор чисел."""
current = start
while True:
yield current
current += step
def id_generator(prefix="id", start=1):
"""Генерирует строковые ID: prefix_1, prefix_2, ..."""
for n in counter(start):
yield f"{prefix}_{n}"
# ─── 2. Класс Paginator ───────────────────────────────────────────────────────
class Paginator:
"""Разбивает данные на страницы фиксированного размера."""
def __init__(self, data, page_size):
self.data = list(data)
self.page_size = page_size
@property
def total_pages(self):
return (len(self.data) + self.page_size - 1) // self.page_size
def __iter__(self):
for i in range(self.total_pages):
start = i * self.page_size
yield self.data[start:start + self.page_size]
def get_page(self, page_num):
if page_num < 1 or page_num > self.total_pages:
return []
start = (page_num - 1) * self.page_size
return self.data[start:start + self.page_size]
# ─── 3. Пагинация с фильтрацией ───────────────────────────────────────────────
def filtered_paginator(data, page_size, predicate):
"""Страницы из элементов, прошедших фильтр."""
page = []
for item in data:
if predicate(item):
page.append(item)
if len(page) == page_size:
yield page
page = []
if page:
yield page
# ─── Демонстрация ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
# 1. Первые 5 ID пользователей
print("=== ID пользователей ===")
users = list(islice(id_generator("user"), 5))
print(users)
# ['user_1', 'user_2', 'user_3', 'user_4', 'user_5']
# 2. Пагинация списка из 13 элементов по 4 штуки
print("\n=== Paginator (13 элементов, страница = 4) ===")
p = Paginator(range(1, 14), page_size=4)
print(f"Всего страниц: {p.total_pages}")
for i, page in enumerate(p, 1):
print(f" Страница {i}: {page}")
# 3. Фильтрация + пагинация
print("\n=== Filtered paginator (чётные, страница = 3) ===")
data = range(1, 21)
for i, page in enumerate(filtered_paginator(data, 3, lambda x: x % 2 == 0), 1):
print(f" Страница {i}: {page}")
🏋️ Самостоятельная работа
Попробуйте решить эти задачи самостоятельно — опираясь на код из урока:
⭐ Задача 1
Напишите генератор fibonacci(), который бесконечно генерирует числа Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13... Выведите первые 10 с помощью islice.
⭐⭐ Задача 2
Добавьте в класс Paginator метод __len__, возвращающий количество страниц, и метод __repr__, который выводит Paginator(items=22, pages=5, page_size=5).
⭐⭐⭐ Задача 3
Напишите генератор chunked_reader(filename, chunk_size), который читает текстовый файл и отдаёт его построчно, страницами по chunk_size строк. Используйте подход из filtered_paginator.
💡 Подсказка к задаче 1: хранить нужно только два последних числа Фибоначчи — a и b. После каждого yield a — обновляете: a, b = b, a + b.
🏁 Итоги урока
- Бесконечный генератор =
while True+yield. Безопасен, потому что ленив — работает только когда его спрашивают - itertools.islice позволяет взять первые N элементов из бесконечного генератора, не «разрывая» его
- Применение бесконечных генераторов: генерация ID, симуляции, потоки событий — всё, где конец заранее неизвестен
- Класс Paginator с методом
__iter__позволяет разбивать данные на страницы и итерироваться по ним как по обычному объекту @property total_pages— вычисляется через формулу(N + size - 1) // size(целочисленное деление с округлением вверх)- filtered_paginator — комбинирует фильтрацию и разбивку на страницы за один проход без промежуточных списков
- Все три паттерна широко используются в реальных проектах: API, базы данных, обработка файлов