Хранение состояния пользователя
FSM запоминает, на каком шаге диалога находится пользователь, а storage — это место, где эта память физически лежит; данные между шагами копят через state.update_data и забирают через state.get_data.
Storage (хранилище состояний) — место, где FSM держит текущие состояния и данные пользователей: память, Redis и т. п.
Зачем тебе это нужно
В прошлом уроке «FSM: машина состояний» наш «Цыплёнок-помощник» научился вести диалог по шагам: спросил имя, дождался ответа, спросил возраст, дождался ответа. FSM как анкета, где поля заполняются по очереди. Но тут возникает важный вопрос, который новички обычно пропускают: а где, собственно, бот всё это запоминает?
Представь: ты пишешь боту имя «Макс», он переходит к вопросу про возраст. Между этими двумя сообщениями могут пройти секунды, а могут и минуты — ты отвлёкся на мем в другом чате. Бот всё это время должен помнить: «этот пользователь сейчас на шаге „ждём возраст“, и кстати его уже зовут Макс». Эта память где-то хранится. Место, где она хранится, называется storage (хранилище состояний), и от выбора storage зависит одна неприятная вещь: переживёт ли бот перезапуск.
Вот к чему мы придём в этом уроке — бот соберёт анкету по кусочкам, а в конце выдаст всё разом:
Бот: Как тебя зовут?
Ты: Макс
Бот: Отлично, Макс! Сколько тебе лет?
Ты: 15
Бот: Записал! Любимый предмет?
Ты: информатика
Бот: 🐤 Готово! Вот твоя анкета:
Имя: Макс
Возраст: 15
Любимый предмет: информатикаРезультат: в чате бот задаёт три вопроса по очереди, на каждый ответ запоминает кусочек данных, а в самом конце собирает всё вместе и присылает заполненную анкету. Чтобы так уметь, нужно понять три вещи: что такое storage и какие они бывают, как докладывать данные на каждом шаге через state.update_data и как забирать всё накопленное через state.get_data. Разберём по порядку.
Storage — это камера хранения для диалогов
Вспомни камеру хранения на вокзале. Ты приходишь с рюкзаком, тебе говорят: «ячейка номер 7, вот ключ». Уходишь гулять налегке, а через час возвращаешься, открываешь свою ячейку — рюкзак на месте. Storage в FSM работает ровно так же. Только вместо рюкзака — твои данные (имя, возраст), а вместо номера ячейки — пара «кто ты + в каком ты чате».
Каждому пользователю FSM выделяет свою «ячейку». В неё кладутся две вещи: текущее состояние (на каком шаге анкеты человек сейчас находится) и накопленные данные (что он уже успел ввести). Когда приходит новое сообщение, aiogram сначала лезет в storage, достаёт «а что мы знали про этого пользователя?», и только потом решает, какой хэндлер вызвать.
State (состояние) — конкретный шаг диалога в FSM, например «ждём имя» или «ждём возраст».
Важно понять: storage и state — это не одно и то же. State — это что лежит в ячейке («сейчас ждём возраст»). Storage — это сама камера хранения, само здание с ячейками. И вот зданий бывает несколько видов, и выбор между ними — главная тема урока.
Два вида камер: MemoryStorage и Redis
В aiogram storage задаётся один раз — при создании диспетчера dp. Самый простой вариант — MemoryStorage, хранилище прямо в оперативной памяти программы.
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=MemoryStorage())Результат: диспетчер dp создаётся с хранилищем в памяти. Бот будет нормально вести диалоги и помнить состояния — ровно до тех пор, пока программа работает.
MemoryStorage — это как камера хранения, которую снесли вместе со всеми ячейками, едва ты ушёл за угол. Звучит дико, но именно так: данные лежат в оперативной памяти процесса. Стоит боту перезапуститься (ты обновил код, упал сервер, кончился интернет) — память очищается, и все «ячейки» исчезают. Пользователь, который был на середине анкеты, после перезапуска окажется как будто впервые зашёл: бот его не помнит.
Второй вариант — Redis. Это отдельная программа-хранилище, которая держит данные так, что они переживают перезапуск твоего бота. Redis как настоящая камера хранения в отдельном здании: твой бот может закрыться на ночь и открыться утром, а ячейки с рюкзаками никуда не делись.
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
bot = Bot(token=TOKEN)
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)Результат: тот же диспетчер dp, но теперь состояния и данные пользователей хранятся в Redis. После перезапуска бота человек, заполнявший анкету, продолжит с того же места — бот его «вспомнит».
Сравним по-честному, когда что брать:
| Хранилище | Переживает перезапуск? | Нужна ли отдельная программа? | Когда брать |
| MemoryStorage | Нет — всё стирается | Нет, работает из коробки | Учёба, тесты, первые боты |
| RedisStorage | Да — данные сохраняются | Да, надо поднять Redis | Боевой бот на сервере |
Для нашего «Цыплёнка-помощника», пока ты учишься и гоняешь его на своём ноутбуке, MemoryStorage — то что надо: ничего ставить не нужно, всё работает сразу. А когда в финале курса мы выкатим бота на настоящий сервер, где он должен жить месяцами и переживать перезапуски, — переедем на Redis. Заметь главное: код хэндлеров при этом не меняется ни на строчку. Ты подменяешь storage в одном месте при создании dp, а вся работа с данными (update_data, get_data) остаётся прежней. В этом и красота: storage — это сменный «движок» под капотом.
Пример 1: докладываем данные через state.update_data
Теперь самое полезное — как класть данные в эту «ячейку». На каждом шаге анкеты мы хотим запомнить ответ пользователя, чтобы в конце собрать всё вместе. Делается это методом state.update_data(...) — он докладывает данные в ячейку, не стирая то, что уже там лежало.
Возьмём наш FSM из прошлого урока. Состояния опишем привычным образом, а в хэндлерах будем по кусочку наполнять «ячейку»:
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
router = Router()
class Form(StatesGroup):
name = State()
age = State()
subject = State()
@router.message(Command("anketa"))
async def start_form(message: Message, state: FSMContext):
await state.set_state(Form.name)
await message.answer("Как тебя зовут?")
@router.message(Form.name)
async def got_name(message: Message, state: FSMContext):
await state.update_data(name=message.text) # докладываем имя
await state.set_state(Form.age)
await message.answer(f"Отлично, {message.text}! Сколько тебе лет?")
@router.message(Form.age)
async def got_age(message: Message, state: FSMContext):
await state.update_data(age=message.text) # докладываем возраст
await state.set_state(Form.subject)
await message.answer("Записал! Любимый предмет?")Результат: в чате на команду /anketa бот спросит имя, после ответа поздоровается по имени и спросит возраст, а затем спросит про любимый предмет. Имя и возраст при этом тихо складываются в «ячейку» пользователя — на экране ты их пока не видишь, но они уже сохранены.
Разберём ключевые места:
state: FSMContext— этот аргумент aiogram сам передаёт в хэндлер.FSMContext— твой «ключ от ячейки» конкретного пользователя: через него и читают, и пишут данные.await state.set_state(Form.name)— переводим пользователя на шаг «ждём имя». Это про состояние, не про данные.await state.update_data(name=message.text)— а вот это про данные. Мы кладём в ячейку пару «name → то, что написал пользователь». Имя ключа (name) придумываешь ты сам.- Дальше
update_data(age=...)добавляет в ту же ячейку второй ключ. Важно: имя никуда не делось —update_dataименно докладывает, а не перезаписывает всё подряд. После двух вызовов в ячейке лежит иname, иage.
Запомни аналогию: update_data — это как кидать вещи в один и тот же рюкзак. Кинул шапку, потом кинул перчатки — шапка осталась на месте, просто рядом легли перчатки.
Пример 2: забираем всё накопленное через state.get_data
Данные мы накопили — пора их достать и показать. Это делает state.get_data(): он возвращает обычный словарь Python со всем, что ты накладывал через update_data. Допишем последний хэндлер анкеты:
@router.message(Form.subject)
async def got_subject(message: Message, state: FSMContext):
await state.update_data(subject=message.text) # докладываем предмет
data = await state.get_data() # забираем всё разом
name = data["name"]
age = data["age"]
subject = data["subject"]
await message.answer(
"🐤 Готово! Вот твоя анкета:\n"
f"Имя: {name}\n"
f"Возраст: {age}\n"
f"Любимый предмет: {subject}"
)
await state.clear() # очищаем ячейкуРезультат: в чате после ответа на третий вопрос бот соберёт все три ответа и пришлёт заполненную анкету: «Имя: Макс / Возраст: 15 / Любимый предмет: информатика». После этого диалог завершается, и ячейка пользователя освобождается.
Что здесь происходит по шагам:
await state.update_data(subject=message.text)— докладываем третий и последний кусочек.data = await state.get_data()— открываем ячейку и забираем весь её содержимый словарь. Теперьdata— это{"name": "Макс", "age": "15", "subject": "информатика"}.data["name"]— достаём конкретные значения по ключам, ровно как из любого словаря.await state.clear()— очень важная строка. Она сбрасывает и состояние, и данные пользователя: ячейка пустеет, диалог считается законченным. Без неё бот так и останется висеть на шаге «ждём предмет».
Раз get_data() возвращает обычный словарь, всё, что ты умеешь делать со словарями в Python, работает и тут. Вот тот же разбор данных анкеты на чистом Python — этот сниппет можно запустить прямо в браузере:
# как будто это вернул state.get_data()
data = {"name": "Макс", "age": "15", "subject": "информатика"}
name = data["name"]
subject = data.get("subject", "не указан")
favorite = data.get("city", "не указан") # такого ключа нет
print("Имя:", name)
print("Предмет:", subject)
print("Город:", favorite)Вывод:
Имя: Макс Предмет: информатика Город: не указан
Обрати внимание на data.get("city", "не указан"): если ключа в словаре нет, .get вернёт запасное значение вместо ошибки. Это пригодится, когда ты не уверен, что нужный кусочек анкеты вообще был заполнен — например, поле необязательное. С data["city"] на отсутствующем ключе программа упала бы с ошибкой, а .get аккуратно подставит дефолт.
Пример 3: можно докладывать сразу несколько полей
Иногда на одном шаге логично сохранить не один кусочек, а сразу пару. update_data легко принимает несколько ключей за раз — просто перечисли их через запятую:
@router.message(Form.name)
async def got_name(message: Message, state: FSMContext):
await state.update_data(
name=message.text,
name_length=len(message.text), # заодно длину имени
)
await state.set_state(Form.age)
await message.answer("Сколько тебе лет?")Результат: в чате бот по-прежнему просто спросит возраст, но в ячейке пользователя теперь окажутся сразу два ключа — name и name_length — добавленные одним вызовом. Снаружи ничего не видно, зато в конце анкеты оба значения доступны через get_data().
Это удобно, когда часть данных ты вычисляешь сам (длину, текущую дату, выбранный язык) и хочешь сохранить рядом с тем, что ввёл пользователь. Главное правило не меняется: каждый вызов update_data добавляет ключи в общую ячейку, а не стирает то, что было раньше.
Частые ошибки новичков
- Создал диспетчер вообще без storage. Если написать
Dispatcher()без аргументаstorage, в свежих версиях aiogram по умолчанию подставится MemoryStorage, но полагаться на это вслепую не стоит. Указывай storage явно:Dispatcher(storage=MemoryStorage())— так сразу видно, какое хранилище у бота, и легче переехать на Redis позже. - Думаешь, что MemoryStorage сохраняет данные навсегда. Самое болезненное открытие новичка. Бот месяц копил данные пользователей, ты перезапустил его ради маленькой правки — и всё пропало. MemoryStorage живёт ровно столько, сколько работает процесс. Нужна память «навсегда» — это Redis (для FSM) или своя база данных (об этом — в модуле про SQLite).
- Перепутал
update_dataиset_data.update_dataдокладывает ключи, сохраняя старые. А вотset_dataзаменяет содержимое ячейки целиком — все прежние ключи стираются. Если случайно позватьset_data(age=...)вместоupdate_data, ранее сохранённое имя исчезнет. В анкетах почти всегда нужен именноupdate_data. - Забыл
await state.clear()в конце. Анкета вроде заполнилась, бот ответил — но пользователь так и остался в состоянии «ждём предмет». Любое его следующее сообщение снова попадёт в последний хэндлер. Завершай диалог черезawait state.clear()— это освобождает ячейку и возвращает пользователя в обычный режим. - Достаёшь данные через
data["key"], которого может не быть. Если поле необязательное и пользователь его пропустил, обращениеdata["city"]уронит хэндлер с ошибкойKeyError. Для необязательных полей бериdata.get("city", "не указан")— вернётся дефолт, и бот не упадёт.
Мини-практика: анкета для игрового клана
Теперь твоя очередь. Возьми текущий bot.py и добавь Цыплёнку команду /zayavka — анкету для вступления в игровой клан. Бот должен по шагам спросить три вещи и в конце собрать заявку:
- Ник в игре — сохрани под ключом
nick. - Любимая игра — сохрани под ключом
game. - Сколько часов в неделю играешь — сохрани под ключом
hours.
Условия задачи: опиши состояния через класс-наследник StatesGroup (как Form в примерах). На каждом шаге докладывай ответ через state.update_data(...), а в последнем хэндлере забери всё через state.get_data() и пришли итоговую заявку вида «Ник: …, Игра: …, Часов в неделю: …». Не забудь в самом конце await state.clear().
Проверь себя по вопросам: какой метод докладывает данные, не стирая старые? Что вернёт get_data() — список, строку или словарь? Что случится, если убрать state.clear()?
Задание для смелых: добавь необязательный четвёртый вопрос — «часовой пояс» — и сохрани его под ключом tz. Сделай так, чтобы при сборке заявки часовой пояс доставался через data.get("tz", "не указан"), и тогда, даже если ты потом решишь этот шаг пропускать, бот не упадёт, а напишет «Часовой пояс: не указан».
Итоги
Сегодня «Цыплёнок-помощник» научился по-настоящему запоминать ход диалога: где это хранится, как туда складывать данные и как их забирать. Главное, что стоит унести с собой:
- Storage — это «камера хранения» FSM: место, где физически лежат и состояние пользователя, и его накопленные данные. Задаётся один раз при создании
dp. - MemoryStorage хранит всё в памяти процесса — просто, без настройки, но данные стираются при каждом перезапуске бота. Идеально для учёбы и тестов.
- RedisStorage хранит данные в отдельной программе, и они переживают перезапуск — выбор для боевого бота на сервере. Код хэндлеров при смене storage не меняется.
state.update_data(key=value)докладывает данные в ячейку пользователя, сохраняя то, что уже там было.state.get_data()возвращает обычный словарь со всем накопленным; значения достаёшь по ключам, а для необязательных полей используешь.get(key, дефолт).state.clear()в конце диалога сбрасывает и состояние, и данные — без него пользователь застрянет на последнем шаге.
В следующем уроке мы научим Цыплёнка делать диалоги умнее: добавим возможность отменить анкету на любом шаге командой /cancel и вернуться назад, а заодно разберём, как мягко обрабатывать неправильные ответы — например, когда вместо возраста пользователь пишет «много». А пока поэкспериментируй с update_data и get_data: именно они превращают набор отдельных вопросов в осмысленную анкету. До встречи! 🐣