Aiogram. Разбираем FSM простыми словами

Автор темы
587
4
Очень часто при написании определённого функционала, определённой цепочки действий, возникает необходимость как-то давать указания боту на что реагировать, а что пропускать мимо ушей. Что сохранять и как всё это совмещать.

Всех приветствую дорогие форумчане! Сегодня мы поговорим про FSM (Finite State Machine), про машину состояний. Что это и как ей управляться. Давайте начнём.

BrjuF8Z.png
​

Представим ситуацию. Вам нужно сделать бота для онлайн покупок. Вот вы написали функционал каталога, пришло время сделать самое основное. Функцию заказа. Вот пользователь вводит название товара, вы его сохраняется в переменную (не дай бог глобальную), далее вы предлагаете пользователю ввести свой адрес. И тут на тебе. А как? Как сделать так, чтобы после того как пользователь ввёл свой адрес, выполнился определённый handler?

Тут к нам на помощь приходит FSM (Finite State Machine).

Как вы могли понять, данная модель помогает передвигаться в определенные функции по скрипту, а также сохранять данные определённого пользователя, у которого активно FSM состояние.

Чтобы начать работать с машиной состояний, для начала нам нужно инициализировать хранилище в dp (Dispatcher)
Python:
from aiogram.contrib.fsm_storage.memory import MemoryStorage

storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)
Отлично! Теперь мы можем записывать данные пользователя в оперативную память.

Теперь давайте поговорим про состояния. Перед тем как создать какое-либо состояние, нам нужно создать класс, где мы поочередно опишем все states чтобы потом без проблем переключаться между ними. Возьмём пример с регистрацией пользователя и вводом его адреса. Создадим класс UserState.
Python:
from aiogram.dispatcher.filters.state import StatesGroup, State


class UserState(StatesGroup):
    name = State()
    address = State()
Тут мы импортируем StatesGroup и State.

После создания класса, мы можем приступать к самому интересному, к основному написанию скрипта. Так что же будет делать бот?

1. спрашивать пользователя его имя
2. записывать имя в переменную состояния
3. переключаться на следующее состояние
4. спрашивать пользователя его адрес
5. записывать адрес в переменную состояния
6. выводить полученную информацию о пользователе
7. очищать состояние пользователя, а также все данные

Для начала создадим handler, который будет обрабатывать команду /reg:
Python:
@dp.message_handler(commands=['reg'])
async def user_register(message: types.Message):
    await message.answer("Введите своё имя")
    await UserState.name.set()
Тут у нас встречается подобная строчка
await UserState.name.set()
Мы обращаемся к классу UserState, далее к состоянию name, а методом set() мы устанавливаем данное состояние. Теперь у нас пользователь имеет состояние UserState.name и мы можем создать handler, который будет реагировать ТОЛЬКО на это состояние. Давайте же создадим такой handler в который пользователь будет записывать своё имя:

Python:
@dp.message_handler(state=UserState.name)
async def get_username(message: types.Message, state: FSMContext):
    await state.update_data(username=message.text)
    await message.answer("Отлично! Теперь введите ваш адрес.")
    await UserState.next() # либо же UserState.address.set()

Итак, давайте разбираться:
async def get_username(message: types.Message, state: FSMContext):
Тут мы вторым аргументом передали state: FSMContext для того, чтобы мы могли записывать данные пользователя в память. Нам также необходимо импортировать:

from aiogram.dispatcher import FSMContext


Далее:
await state.update_data(username=message.text)
Здесь мы вызываем метод update_data(), который сохраняет данные в память, где единственным аргументом мы указываем название ключа в памяти 'username', а также его значение 'message.text'.

Далее просим пользователя ввести его адрес, а после, неявно переключаем состояние на следующее (а следующий state у нас 'adress') с помощью метода

await UserState.next()


Однако переключайте состояния таким способом только тогда, когда точно знаете какой state (в классе) идёт дальше.

Также можно переключать состояния явно:
await
UserState.address.set()


Отлично:claps: Теперь создадим финальный хэндлер где будем получать адрес пользователя, а также выводить ему всю имеющуюся информацию.
Python:
@dp.message_handler(state=UserState.address)
async def get_address(message: types.Message, state: FSMContext):
    await state.update_data(address=message.text)
    data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
                         f"Адрес: {data['address']}")

    await state.finish()
В начале в принципе мы с вами уже всё разобрали, state, FSMContext, update_data. Но тут у нас появляется новый актер.
data = await state.get_data()
С помощью метода get_data() мы получаем все ранее записанные в состояние данные, и сохраняем всё в переменную data. Теперь мы можем получить данные состояний по ключу через переменную data. Что.. собственно мы и делаем в следующей строчке при выводе всей информации пользователю.
Python:
    data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
                         f"Адрес: {data['address']}")

И в конце концов мы всё завершаем методом finish()
await state.finish()
Данный метод очищает все состояния пользователя, а также удаляет все ранее сохраненные данные. Если же вам надо сбросить только состояние, воспользуйтесь:
await state.reset_state(with_data=False)

Весь код:
Python:
from aiogram import types
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup


class UserState(StatesGroup):
    name = State()
    address = State()


@dp.message_handler(commands=['reg'])
async def user_register(message: types.Message):
    await message.answer("Введите своё имя")
    await UserState.name.set()

  
@dp.message_handler(state=UserState.name)
async def get_username(message: types.Message, state: FSMContext):
    await state.update_data(username=message.text)
    await message.answer("Отлично! Теперь введите ваш адрес.")
    await UserState.next() # либо же UserState.adress.set()


@dp.message_handler(state=UserState.address)
async def get_address(message: types.Message, state: FSMContext):
    await state.update_data(address=message.text)
    data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
                         f"Адрес: {data['address']}")

    await state.finish()

Итог
2VjZYov.png

Друзья, надеюсь всем всё понятно разъяснил, если понравилась статья, дайте об этом знать:sobaken:

На этом всё, всем удачи и светлого ума! До скорого.