08 — FSM — holatlar mashinasi¶
⬅️ Oldingi: 07 — Callback query va inline rejim · 🏠 README · Keyingi: 09 — Middleware ➡️
Bu bobda: Hozirgacha har bir xabar mustaqil edi — bot xabarni oldi, javob berdi, unutdi. Lekin ko'p real botlar suhbat olib boradi: "Ismingizni yozing" -> "Yoshingizni yozing" -> "Shahringizni yozing". Bot avval qaysi savol berganini, foydalanuvchi qaysi qadamda turganini eslab turishi kerak. Buni FSM (Finite State Machine — chekli holatlar mashinasi) hal qiladi. Bu bobda holat (state) nima ekanini,
StatesGroupvaStatebilan holatlar to'plamini e'lon qilishni, handlergaFSMContextorqali kirib holatni boshqarishni (set_state,get_state,set_data/update_data/get_data/get_value,clear), holat qaerda saqlanishini (MemoryStoragevsRedisStorage), to'liq ko'p qadamli so'rovnoma/forma yasashni, har qanday holatdan/cancelbilan chiqishni va holatlar diagrammasini o'rganamiz. Oxirida yana bir muhim nozik joy — state handler buyruqni "yutib yuborishi" muammosini ham yopamiz.Halol eslatma: Bobdagi holat o'tishlari (state transitions),
FSMContextmetodlari,StatesGroup/State,MemoryStorage, ko'p qadamli forma va/cancelmantiqi — hammasi tokensiz, OFFLINEfeed_updatevapytest-asynciobilan haqiqatan ishga tushirib tekshirildi (bot sessiyasi soxta token bilan, hech qanday tarmoq chaqiruvisiz). Jonli natija — telefonda savol-javob ketma-ketligi ko'rinishi —@BotFathertoken + internet talab qiladi va "illustrativ" deb belgilangan. Hech qayerda soxta "bot ishladi / xabar yetib bordi" yozilmagan.
Holat (state) nima va nega kerak?¶
Tasavvur qiling, bot foydalanuvchidan ro'yxatdan o'tish uchun uch narsa so'raydi: ism, yosh, shahar. Foydalanuvchi "Oqil" deb yozadi. Bot buni nima deb tushunsin? Ism deb? Yosh deb? Bot kontekstni bilishi kerak: "men hozir foydalanuvchidan ISMni so'ragan edim, demak bu javob — ism".
Ana shu "men hozir qaysi qadamdaman" degan ma'lumot — holat (state). FSM (chekli holatlar mashinasi) — bu cheklangan sondagi holatlar va ular orasidagi o'tishlardan iborat model:
- Foydalanuvchi har lahzada bitta holatda turadi (yoki hech qaysi holatda — "bo'sh").
- Har bir javob foydalanuvchini keyingi holatga o'tkazadi.
- Forma tugagach holat tozalanadi — foydalanuvchi yana "bo'sh" holatga qaytadi.
Python eslatma: Bu kitob Python bilasiz deb faraz qiladi (async/await, klass, type hints, dekorator). Agar bu tushunchalar yangi bo'lsa, avval ../python/README.md ga qarang. FSM — bu Telegram/aiogram'ga xos narsa emas, umumiy dasturlash tushunchasi; biz uni aiogram'da qanday ishlatishni to'liq tushuntiramiz.
Eng muhim narsa: holat har bir foydalanuvchi uchun alohida saqlanadi. Bir vaqtda yuz odam botingiz bilan ishlashi mumkin — biri ism qadamida, ikkinchisi yosh qadamida. aiogram buni avtomatik ajratib turadi (kalit = bot + chat + foydalanuvchi).
StatesGroup va State — holatlarni e'lon qilish¶
aiogram'da holatlar StatesGroup klassidan meros olgan klass ichida State() obyektlari sifatida e'lon qilinadi. Bu xuddi Python Enum'iga o'xshaydi — har bir holatga nom beramiz.
from aiogram.fsm.state import State, StatesGroup
class Survey(StatesGroup):
name = State() # ism so'raladigan holat
age = State() # yosh so'raladigan holat
city = State() # shahar so'raladigan holat
Har bir State ichki nom oladi — "GuruhNomi:atributNomi" ko'rinishida. Tekshirib ko'ramiz (oddiy import, tokensiz):
print(Survey.name) # <State 'Survey:name'>
print(Survey.name.state) # Survey:name <- satr ko'rinishi
print(Survey.__states__) # (<State 'Survey:name'>, <State 'Survey:age'>, <State 'Survey:city'>)
Survey.name.state aynan "Survey:name" satrini beradi — storage'da holat shu satr sifatida saqlanadi. Bu detalni eslab qolish shart emas, lekin nima saqlanishini bilish foydali.
Diqqat (2.x emas): aiogram 2.x'da ham
StatesGroup/Statebor edi, lekin holatga kirish/chiqish API'si butunlay boshqacha edi. Biz faqat 3.x idiomini ishlatamiz. Eskifrom aiogram.dispatcher.filters.state import State, StatesGroupimport yo'lini ishlatmang — to'g'risifrom aiogram.fsm.state import State, StatesGroup.
FSMContext — holatni boshqarish¶
Handler holat bilan ishlashi uchun unga FSMContext obyekti kerak. aiogram buni avtomatik inject qiladi: handler funksiyasi argumentiga state: FSMContext deb yozsangiz, aiogram joriy foydalanuvchining holat kontekstini o'zi beradi.
from aiogram.fsm.context import FSMContext
async def handler(message: Message, state: FSMContext):
await state.set_state(Survey.name) # holatni o'rnatish
FSMContextning asosiy metodlari (hammasi async, await bilan chaqiriladi):
| Metod | Vazifasi |
|---|---|
await state.set_state(Survey.name) |
Joriy holatni o'rnatadi. set_state(None) — holatni bo'shatadi (data tegmaydi). |
await state.get_state() |
Joriy holat satrini qaytaradi ("Survey:name") yoki None (holat yo'q). |
await state.update_data(name="Oqil") |
Mavjud data'ga qo'shadi/yangilaydi (merge). Yangilangan to'liq data'ni qaytaradi. |
await state.set_data({"name": "Oqil"}) |
Butun data'ni almashtiradi (eskisi o'chadi). |
await state.get_data() |
Barcha saqlangan data'ni dict ko'rinishida qaytaradi. |
await state.get_value("name") |
Bitta kalit qiymatini qaytaradi (yoki default). |
await state.clear() |
Holat VA data — ikkalasini ham tozalaydi. |
update_data va set_data farqini aniq bilib oling:
await state.set_data({"name": "Oqil"}) # data = {"name": "Oqil"}
await state.update_data(age=30) # data = {"name": "Oqil", "age": 30} <- qo'shildi
await state.update_data(name="Imom") # data = {"name": "Imom", "age": 30} <- yangilandi
await state.set_data({"city": "Toshkent"}) # data = {"city": "Toshkent"} <- HAMMASI almashdi!
Amaliyotda deyarli har doim update_data ishlatiladi — har qadamda bittadan qiymat qo'shib boramiz. set_data ni faqat ataylab hammasini tozalab qaytadan yozmoqchi bo'lsangiz ishlating.
Nega
clear()? Ko'pchilik faqatset_state(None)qilib qo'yadi va data'ni unutadi. Forma tugagachclear()chaqiring — aks holda eski data (ism, yosh) storage'da qolib ketadi va keyingi formada chalkashlik tug'diradi.clear()=set_state(None)+ data'ni tozalash.
Holat va data qayerda saqlanadi? — Storage¶
FSMContext o'zi holatni saqlamaydi — u faqat Storagega interfeys. Storage — bu "kalit -> (holat, data)" lug'ati. Kalit har bir foydalanuvchi uchun bot_id + chat_id + user_id dan tuziladi, shuning uchun foydalanuvchilar bir-biriga aralashmaydi.
Storage Dispatcherga beriladi:
from aiogram import Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
dp = Dispatcher(storage=MemoryStorage())
Default: Agar
storage=bermasangiz,Dispatcher()baribirMemoryStorage()ishlatadi. Ya'niDispatcher()vaDispatcher(storage=MemoryStorage())FSM nuqtai nazaridan bir xil. Lekin kodingizda aniq yozib qo'yish — yaxshi odat. (Buni offline tasdiqladik:Dispatcher()ning standart storage'iMemoryStorage.)
MemoryStorage vs RedisStorage¶
MemoryStorage |
RedisStorage |
|
|---|---|---|
| Qayerda | Bot protsessining RAM'ida (oddiy dict) |
Tashqi Redis serverda |
| Bot qayta ishga tushsa | Hamma holat/data o'chadi | Saqlanadi |
| Ko'p protsess/server | Ishlamaydi (har biri o'z RAM'i) | Ishlaydi (umumiy markaz) |
| Qachon | O'rganish, lokal sinov, kichik bot | Production, restart'ga chidamli, gorizontal masshtab |
| Talab | Hech narsa | pip install redis + ishlab turgan Redis |
RedisStorage ishlatish (illustrativ — Redis server kerak; import yo'li 3.x'da to'g'ri):
# Bu uchun "pip install redis" kerak va Redis server ishlab turishi shart.
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)
MemoryStorage bilan bot to'xtab qayta ishga tushsa, yarmida qolgan forma "yo'qoladi" — foydalanuvchi navbatdagi savolga javob yozsa, bot uni tushunmaydi (chunki holat o'chgan). Production'da RedisStorage shuning uchun afzal: bot deploy paytida restart bo'lsa ham, suhbat joyida qoladi.
Eslatma: Bu kitobning qolgan misollarida
MemoryStorageishlatamiz — u tokensiz, hech qanday tashqi xizmatsiz ishlaydi va o'rganish uchun ideal. Production'ga chiqishda faqat shu bir qatorniRedisStoragega almashtirasiz, qolgan kod o'zgarmaydi. Redis haqida ko'proq: deploy bobida (../git-github/README.md) va ma'lumotlar bazasi mavzusi (../sql/README.md) bilan bog'liq.
Holatga kirish va holatdan holatga o'tish¶
Endi eng qiziq qism — handler holatga bog'lanadi. aiogram'da holat filtri shunchaki handler dekoratoriga holatni argument qilib berishdan iborat:
@router.message(Survey.name) # faqat foydalanuvchi Survey.name holatida bo'lsa ishlaydi
async def handler(message, state): ...
Bu StateFilter(Survey.name) ning qisqartmasi. Ya'ni quyidagi ikkisi bir xil:
from aiogram.filters import StateFilter
@router.message(Survey.name) # qisqa shakl
@router.message(StateFilter(Survey.name)) # to'liq shakl — bir xil natija
Mantiq oddiy: foydalanuvchidan xabar kelganda, aiogram avval uning joriy holatini storage'dan o'qiydi, keyin holatga mos handlerni qidiradi.
StateFilterning maxsus qiymatlari:
StateFilter(None)— faqat foydalanuvchi hech qaysi holatda bo'lmasa (forma boshlamagan).StateFilter("*")— istalgan holatda (/canceluchun juda qulay, pastda ko'ramiz).StateFilter(A, B)— bir nechta holatdan birida.
To'liq ko'p qadamli so'rovnoma (forma)¶
Mana to'liq, ishlaydigan misol — uch qadamli so'rovnoma. Buni biz OFFLINE feed_update bilan haqiqatan ishga tushirib tekshirdik (pastda natija bor).
# survey.py — uch qadamli so'rovnoma (FSM)
import asyncio
import os
from aiogram import Bot, Dispatcher, Router, F
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.filters import CommandStart
from aiogram.types import Message
router = Router()
class Survey(StatesGroup):
name = State()
age = State()
city = State()
@router.message(CommandStart())
async def start(message: Message, state: FSMContext):
await state.set_state(Survey.name) # 1-qadamga kiramiz
await message.answer("Salom! Ismingizni yozing:")
# Foydalanuvchi Survey.name holatida va matn yuborsa:
@router.message(Survey.name, F.text)
async def got_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(Survey.age) # keyingi qadamga
await message.answer(f"Yaxshi, {message.text}! Yoshingizni yozing (faqat raqam):")
# Survey.age holatida VA matn faqat raqamlardan iborat bo'lsa:
@router.message(Survey.age, F.text.regexp(r"^\d{1,3}$"))
async def got_age(message: Message, state: FSMContext):
await state.update_data(age=int(message.text))
await state.set_state(Survey.city)
await message.answer("Qaysi shaharda yashaysiz?")
# Survey.age holatida, lekin raqam EMAS bo'lsa — holatda qolamiz, qayta so'raymiz:
@router.message(Survey.age)
async def bad_age(message: Message):
await message.answer("Iltimos, yoshni faqat raqam bilan yozing. Masalan: 25")
# Survey.city holatida matn kelsa — forma tugaydi:
@router.message(Survey.city, F.text)
async def got_city(message: Message, state: FSMContext):
await state.update_data(city=message.text)
data = await state.get_data() # to'plangan hammasini olamiz
await state.clear() # holat + data tozalanadi
await message.answer(
"Rahmat! Ma'lumotlaringiz:\n"
f"Ism: {data['name']}\n"
f"Yosh: {data['age']}\n"
f"Shahar: {data['city']}"
)
async def main():
token = os.environ["BOT_TOKEN"] # token KODGA yozilmaydi, .env dan
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
await dp.start_polling(bot) # <- jonli qism, token+internet kerak
if __name__ == "__main__":
asyncio.run(main())
Diqqat qiladigan nuqtalar:
F.textfiltri — foydalanuvchi rasm yoki stiker yuborsa,F.textmos kelmaydi, shuning uchun matn bo'lmagan javoblar e'tiborsiz qoladi (yoki alohida handlerda ushlanadi).- Validatsiya holatda qolish bilan —
got_ageraqamni talab qiladi. Raqam bo'lmasa,bad_ageishlaydi: uset_stateham,update_dataham qilmaydi — foydalanuvchiSurvey.ageholatida qoladi va qayta urinadi. Bu FSM'ning eng kuchli xususiyati. - Handler tartibi —
got_age(raqam filtri)bad_age'dan oldin turishi shart. aiogram birinchi mos kelganini ishlatadi.
Buni qanday OFFLINE tekshirdik¶
main() (start_polling) jonli Telegram talab qiladi — uni token+internetsiz ishga tushira olmaymiz. Lekin handler mantiqini soxta token bilan, feed_update orqali to'liq tekshirsa bo'ladi. Biz aynan shu so'rovnomani quyidagi sxema bilan sinovdan o'tkazdik:
# test_survey.py — OFFLINE, tokensiz (faqat handler/FSM mantig'ini tekshiradi)
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, Message, Chat, User
# ... yuqoridagi router shu yerda import qilinadi ...
def make_update(uid, text):
msg = Message(
message_id=uid, date=datetime.now(),
chat=Chat(id=99, type="private"),
from_user=User(id=99, is_bot=False, first_name="Test"),
text=text,
)
return Update(update_id=uid, message=msg)
async def main():
bot = Bot(token="123456:AAH-FakeTest_abc") # SOXTA token — tarmoqqa chiqmaydi
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
for i, t in enumerate(["/start", "Oqil", "yosh emas", "30", "Toshkent"], 1):
await dp.feed_update(bot, make_update(i, t))
await bot.session.close()
asyncio.run(main())
Holat o'tishlarini log qilib kuzatganimizda, aniq quyidagi ketma-ketlik chiqdi (bu haqiqiy offline natija):
('start', 'Survey:name') # /start -> name holati
('name', 'Survey:age') # "Oqil" -> age holati
('bad_age','Survey:age') # "yosh emas" -> age'da QOLDI
('age', 'Survey:city') # "30" -> city holati
('city', {'name': 'Oqil', 'age': 30, 'city': 'Toshkent'}, None) # tugadi, clear()
Ya'ni "yosh emas" raqam bo'lmagani uchun bad_age ishladi va holat o'zgarmadi — keyin "30" to'g'ri qabul qilindi. Forma oxirida data to'liq yig'ildi ({'name': 'Oqil', 'age': 30, 'city': 'Toshkent'}) va clear() dan keyin holat None bo'ldi. Bu — soxta emas, sinov tom ma'noda shunday natija berdi.
Jonli ko'rinish (illustrativ — token+internet kerak): Telefonda bu shunday ko'rinardi — bot "Ismingizni yozing" deydi, siz "Oqil" deysiz, bot "Yoshingizni yozing" deydi, va hokazo. Biz buni real Telegram'da ishga tushirmadik (token kerak), lekin handler mantig'i yuqoridagidek aniq tekshirilgan.
Holatdan chiqish va bekor qilish — /cancel¶
Foydalanuvchi forma o'rtasida fikridan qaytishi mumkin. Unga "chiqish yo'li" berish shart — aks holda u boshqa hech narsa qila olmay qoladi (bot doim savol kutib turadi). Buning uchun istalgan holatda ishlaydigan /cancel handler yozamiz:
from aiogram.filters import Command, StateFilter
@router.message(StateFilter("*"), Command("cancel"))
async def cancel(message: Message, state: FSMContext):
current = await state.get_state()
if current is None:
await message.answer("Bekor qiladigan amal yo'q.")
return
await state.clear()
await message.answer("Amal bekor qilindi. Boshidan boshlash uchun /start.")
Bu yerda:
StateFilter("*")— bu handler istalgan holatda ishlaydi (name,age,city— farqi yo'q).get_state()Nonebo'lsa — foydalanuvchi hech qaysi formada emas, bekor qiladigan narsa yo'q, shunchaki xabar beramiz.- Aks holda
clear()— holat va data tozalanadi, foydalanuvchi "bo'sh" holatga qaytadi.
"bekor" so'zini ham (tugma bosib yuboriladigan) qabul qilmoqchi bo'lsangiz, bir handlerga ikki filtr qo'shasiz (OR mantig'i — bir nechta dekorator bilan):
@router.message(StateFilter("*"), Command("cancel"))
@router.message(StateFilter("*"), F.text.casefold() == "bekor")
async def cancel(message: Message, state: FSMContext):
...
Ikkala dekorator ham bitta funksiyaga ulanadi — /cancel buyrug'i YOKI "bekor" matni — ikkalasi ham shu handlerga olib keladi. Biz /cancel ni ham mid-flow (forma o'rtasida), ham bo'sh holatda OFFLINE tekshirdik: mid-flow'da holat None'ga tushdi, bo'sh holatda esa "noop" (hech narsa qilmadi) ishladi.
Nozik joy: state handler buyruqni "yutib yuborishi"¶
Bu — ko'p odam yo'l qo'yadigan, lekin kam yoziladigan xato. Quyidagi handlerni ko'ring:
# ❌ MUAMMOLI: bu handler Survey.age holatidagi HAR QANDAY matnni ushlaydi
@router.message(Survey.age)
async def got_age(message, state):
...
Foydalanuvchi Survey.age holatida turib /cancel yozsa, bu matn ham Survey.age holatdagi matn hisoblanadi! Agar got_age /cancel handleridan oldin ro'yxatdan o'tgan bo'lsa (yoki shu router birinchi tekshirilsa), /cancel "yutib yuboriladi" — cancel handler hech qachon ishlamaydi.
Yechim ikki xil:
1) /cancel ni global, FSM holatlaridan tashqarida e'lon qiling va uni router/handler tartibida oldinroqqa qo'ying (StateFilter("*") bilan, biz yuqorida shunday qildik).
2) State javob handlerlaridan buyruqlarni chiqarib tashlang — magic-filter bilan:
# ✅ buyruqlarni (/ bilan boshlanadigan) bu handler ushlamaydi
@router.message(Survey.age, ~F.text.startswith("/"))
async def got_age(message, state):
...
~F.text.startswith("/") — "matn / bilan boshlanMAsa" degani (~ — inkor). Endi /cancel, /start kabi buyruqlar got_age'ga tushmaydi va o'z handleriga yetib boradi.
Biz buni OFFLINE tasdiqladik: ikki foydalanuvchi parallel formada turganda (biri q1, biri q2 holatida), ~F.text.startswith("/") filtri bo'lmaganda /where buyrug'i state handlerga "yutilib ketdi"; filtr qo'shilgach buyruq to'g'ri ishladi. Ya'ni bu — nazariy emas, real xulq-atvor.
Qoida: State javob handlerlari odatda
F.textyoki~F.text.startswith("/")bilan cheklanadi; barcha buyruqlar (/cancel,/start,/help) esaStateFilter("*")bilan alohida e'lon qilinadi va router ichida oldinroqqa qo'yiladi.
Bir foydalanuvchi = bir holat (izolyatsiya)¶
Yana bir bor ta'kidlaymiz, chunki bu juda muhim: holat kalit bo'yicha saqlanadi (bot + chat + user). Demak:
- Foydalanuvchi A
Survey.nameholatida, foydalanuvchi BSurvey.ageholatida bo'lishi mumkin — bir-biriga ta'sir qilmaydi. - Bir foydalanuvchi data'si boshqasiga aralashmaydi.
Buni OFFLINE ikki xil user_id bilan tekshirdik: foydalanuvchi 100 formani davom ettirib q2'ga o'tdi, foydalanuvchi 200 esa hali q1'da qoldi — get_state() har biriga to'g'ri qiymat qaytardi. Bu sizning kodingizda hech qanday qo'shimcha mehnat talab qilmaydi — aiogram avtomatik bajaradi.
Node.js bilan solishtirish: Agar Node.js bot freymvorklarini ko'rgan bo'lsangiz (../nodejs/README.md), u yerda ko'pincha sessiya/state'ni o'zingiz
ctx.sessionorqali boshqarasiz. aiogram'da FSM bu ishni tuzilgan holat mashinasi sifatida, holat filtrlari bilan birlashtirib beradi — bu kodni ancha tartibli qiladi.
Yo'l xaritasi: forma yozishning standart shabloni¶
Har safar ko'p qadamli forma yozganda shu qadamlarni bajaring:
StatesGroupichida har bir savol uchun bittaStatee'lon qiling.- Boshlovchi handler (
/startyoki tugma) —set_state(birinchi_holat). - Har bir holat uchun handler:
update_data(...)->set_state(keyingi_holat). Kerak bo'lsa validatsiya handlerini (holatda qoldiradigan) qo'shing. - Oxirgi holatda:
get_data()-> ishni bajaring (DB'ga yozish va h.k.) ->clear(). StateFilter("*")bilan/cancelhandler qo'shing.- State javob handlerlarida buyruqlarni filtrlang (
~F.text.startswith("/")). Dispatcher(storage=MemoryStorage())(production'daRedisStorage).
Mashqlar¶
Oson¶
RegnomliStatesGroupyozing, ichidafirst_namevalast_nameikkitaState.Reg.first_name.statenimani qaytaradi? Avval taxmin qiling, keyinprintbilan tekshiring.MemoryStoragevaRedisStorageorasidagi ENG asosiy farqni bir jumlada ayting: bot qayta ishga tushganda nima bo'ladi?update_data(x=1)keyinupdate_data(y=2)chaqirilsa,get_data()nima qaytaradi? Endi o'rniga ikkinchisiniset_data({"y": 2})qilsangiz-chi?clear()vaset_state(None)orasidagi farq nima? Qaysi birida data saqlanib qoladi?- Bitta savolli "fikr-mulohaza" (feedback) formasi yozing:
/feedback-> bot "Fikringizni yozing" deydi -> foydalanuvchi matn yuboradi -> bot "Rahmat!" deydi va holatni tozalaydi. StateFilter("*"),StateFilter(None)vaStateFilter(Survey.age)— har biri qachon mos keladi? Bir jumladan tushuntiring.
O'rta¶
/loginformasi yozing: avvalusername, keyinpasswordso'raydi, oxiridaupdate_datanatijasidan ikkalasini olib "Xush kelibsiz, {username}!" deb javob beradi.RedisStorageemas,MemoryStorageishlating.- 7-mashqdagi
/loginformasigaStateFilter("*")bilan/cancelqo'shing: forma o'rtasida/cancelyozilsa, holat tozalanib "Bekor qilindi" deyilsin; bo'sh holatda yozilsa "Bekor qiladigan amal yo'q" deyilsin. Survey.ageholatida foydalanuvchi raqam o'rniga matn yozsa, holatda qolib qayta so'raydigan validatsiya handler yozing. Raqam 1..120 oralig'ida ekanini ham tekshiring (oraliqdan tashqari bo'lsa ham qayta so'rasin).got_agehandler/cancelni "yutib yubormasligi" uchun unga to'g'ri magic-filter qo'shing va nega kerakligini izohlang.- Forma oxirida
get_data()bilan to'plangan barcha javoblarni bitta formatli xabarga jamlab chiqaring (ism, yosh, shahar). Agar shahar kiritilmagan bo'lsa "ko'rsatilmagan" deb yozsin (get_value("city", "ko'rsatilmagan")). set_state(Survey.age)chaqirgandan keyin, lekinupdate_data'siz,get_data()nima qaytaradi? Holatni o'zgartirish data'ni o'zgartiradimi? Taxmin qiling va offline tekshiring.
Qiyin¶
- So'rovnomaga "orqaga" tugmasi qo'shing:
qty(miqdor) holatida foydalanuvchi "orqaga" desa,productholatiga qaytsin (oldingi savolni qayta bersin). Holat tarixini (product -> qty -> product -> qty) kuzatib tekshiring. - Ikki xil foydalanuvchi (
user_id100 va 200) parallel bir formani to'ldirayotganda holatlari aralashmasligini OFFLINEfeed_updatebilan isbotlang. 100 ikkinchi qadamda, 200 birinchi qadamda turganiniget_state()bilan ko'rsating. - Forma to'liq tugagach, to'plangan data'ni soxta "saqlash" funksiyasiga (
save_to_db(data)— shunchaki ro'yxatga qo'shadigan) uzating, keyinclear()qiling.pytest-asynciotesti yozib, ro'yxatda to'g'ri yozuv borligini va holatNoneekanini tasdiqlang. /cancelni bitta handlerga hamCommand("cancel"), hamF.text.casefold() == "bekor"filtri bilan ulang (ikki dekorator). Har ikki kirish ham holatni tozalashini tekshiring.
Yechimlar
1-mashq.
from aiogram.fsm.state import State, StatesGroup
class Reg(StatesGroup):
first_name = State()
last_name = State()
print(Reg.first_name.state) # "Reg:first_name"
Holat satri har doim "GuruhNomi:atributNomi" ko'rinishida bo'ladi. Shuning uchun Reg.first_name.state aynan "Reg:first_name" ni qaytaradi.
2-mashq. MemoryStorage holatni bot protsessining RAM'ida saqlaydi — bot qayta ishga tushsa hamma holat va data o'chadi. RedisStorage tashqi Redis serverda saqlaydi — bot restart bo'lsa ham holat saqlanadi. Demak yarmida qolgan formalar production'da yo'qolmasligi uchun RedisStorage kerak.
3-mashq.
await state.update_data(x=1) # data = {"x": 1}
await state.update_data(y=2) # data = {"x": 1, "y": 2} <- merge, "x" saqlandi
await state.get_data() # {"x": 1, "y": 2}
# Endi set_data bilan:
await state.update_data(x=1) # data = {"x": 1}
await state.set_data({"y": 2}) # data = {"y": 2} <- HAMMASI almashdi, "x" yo'qoldi
update_data qo'shadi/yangilaydi (merge), set_data esa butun data'ni almashtiradi.
4-mashq. set_state(None) faqat holatni bo'shatadi — saqlangan data (update_data bilan yozilgan) qoladi. clear() esa holatni ham, data'ni ham ikkalasini ham tozalaydi. Forma tugagach clear() ishlatish kerak, aks holda eski data keyingi suhbatda chalkashlik tug'diradi.
5-mashq.
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
class Fb(StatesGroup):
text = State()
@router.message(Command("feedback"))
async def start(message: Message, state: FSMContext):
await state.set_state(Fb.text)
await message.answer("Fikringizni yozing:")
@router.message(Fb.text, F.text)
async def got(message: Message, state: FSMContext):
# bu yerda fikrni saqlash mumkin: print(message.text) yoki DB'ga
await state.clear()
await message.answer("Rahmat!")
feed_update bilan ["/feedback", "Zo'r bot!"] yuborganimizda fikr to'g'ri ushlandi va holat tozalandi (offline, pytest-asyncio bilan tasdiqlangan).
6-mashq.
- StateFilter("*") — foydalanuvchi istalgan holatda bo'lganda mos keladi; global /cancel uchun ishlatiladi.
- StateFilter(None) — foydalanuvchi hech qaysi holatda bo'lmaganda (forma boshlamagan).
- StateFilter(Survey.age) — faqat aynan Survey.age holatida.
7-mashq.
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
class Login(StatesGroup):
username = State()
password = State()
@router.message(Command("login"))
async def start(message: Message, state: FSMContext):
await state.set_state(Login.username)
await message.answer("Login (username) yozing:")
@router.message(Login.username, ~F.text.startswith("/"))
async def uname(message: Message, state: FSMContext):
await state.update_data(username=message.text)
await state.set_state(Login.password)
await message.answer("Parolni yozing:")
@router.message(Login.password, ~F.text.startswith("/"))
async def pwd(message: Message, state: FSMContext):
data = await state.update_data(password=message.text)
await state.clear()
await message.answer(f"Xush kelibsiz, {data['username']}!")
["/login", "oqil", "secret"] bilan tekshirilganda data == {"username": "oqil", "password": "secret"} chiqdi (offline, pytest-asyncio). Diqqat: parolni real botda yodda saqlamaslik/maxfiy ishlash kerak — bu o'quv misol.
8-mashq.
from aiogram.filters import StateFilter
@router.message(StateFilter("*"), Command("cancel"))
async def cancel(message: Message, state: FSMContext):
if await state.get_state() is None:
await message.answer("Bekor qiladigan amal yo'q.")
return
await state.clear()
await message.answer("Bekor qilindi.")
Bu handlerni Login handlerlaridan oldin (router ichida yuqorida) qo'ying, va uname/pwd'da ~F.text.startswith("/") bo'lgani uchun /cancel ularga tushmaydi. ["/login", "oqil", "/cancel"] -> holat tozalandi, "login" data yozilmadi (offline tasdiqlangan).
9-mashq.
@router.message(Survey.age, F.text.regexp(r"^\d{1,3}$"))
async def got_age(message: Message, state: FSMContext):
age = int(message.text)
if not (1 <= age <= 120):
await message.answer("Yosh 1 dan 120 gacha bo'lishi kerak. Qayta yozing:")
return # holatni o'zgartirmaymiz -> Survey.age'da qoladi
await state.update_data(age=age)
await state.set_state(Survey.city)
await message.answer("Shahringiz?")
@router.message(Survey.age) # raqam umuman emas (regexp mos kelmadi)
async def bad_age(message: Message):
await message.answer("Faqat raqam yozing. Masalan: 25")
Ikki bosqichli tekshiruv: regexp raqam emasligini, ichki if esa oraliqni tekshiradi. Har ikkalasi ham set_state qilmagani uchun foydalanuvchi Survey.age'da qoladi.
10-mashq.
@router.message(Survey.age, ~F.text.startswith("/"))
async def got_age(message: Message, state: FSMContext):
...
~F.text.startswith("/") — matn / bilan boshlanmasa. Bu kerak, chunki aks holda Survey.age holatidagi foydalanuvchi /cancel yozsa, bu matn ham "yosh javobi" deb got_age'ga tushib ketadi va global /cancel handler ishlamaydi (state handler buyruqni "yutib yuboradi"). Filtr buyruqlarni chiqarib tashlaydi.
11-mashq.
@router.message(Survey.city, F.text)
async def done(message: Message, state: FSMContext):
await state.update_data(city=message.text)
data = await state.get_data()
city = await state.get_value("city", "ko'rsatilmagan")
await state.clear()
# Apostrof tufayli f-string ichida backslash ISHLATMANG -- SyntaxError beradi.
# Qiymatlarni oldindan o'zgaruvchiga hisoblang, keyin f-string'ga qo'ying.
na = "ko'rsatilmagan"
name = data.get("name", na)
age = data.get("age", na)
await message.answer(
"Ma'lumotlaringiz:\n"
f"Ism: {name}\n"
f"Yosh: {age}\n"
f"Shahar: {city}"
)
get_value("city", "ko'rsatilmagan") — agar city kaliti yo'q bo'lsa default qaytaradi. dict.get(...) ham xuddi shu ishni qiladi.
12-mashq.
set_state faqat holatni o'zgartiradi, data alohida saqlanadi. update_data chaqirmaguningizcha data bo'sh ({}) qoladi. Offline tasdiqlandi: holat o'zgardi, data {} bo'lib qoldi.
13-mashq.
class Order(StatesGroup):
product = State()
qty = State()
@router.message(Command("order"))
async def start(message: Message, state: FSMContext):
await state.set_state(Order.product)
await message.answer("Mahsulot nomi?")
@router.message(Order.product, ~F.text.startswith("/"))
async def prod(message: Message, state: FSMContext):
await state.update_data(product=message.text)
await state.set_state(Order.qty)
await message.answer("Miqdori? (raqam, yoki 'orqaga')")
@router.message(Order.qty, F.text.casefold() == "orqaga")
async def back(message: Message, state: FSMContext):
await state.set_state(Order.product) # oldingi holatga
await message.answer("Mahsulot nomini qayta yozing:")
@router.message(Order.qty, F.text.regexp(r"^\d+$"))
async def qty(message: Message, state: FSMContext):
data = await state.update_data(qty=int(message.text))
await state.clear()
await message.answer(f"Buyurtma: {data['product']} x {data['qty']}")
["/order", "Olma", "orqaga", "Nok", "5"] bilan holat tarixi ["Order:qty", "Order:product", "Order:qty"] bo'ldi va yakuniy data {"product": "Nok", "qty": 5} chiqdi — "orqaga" eski mahsulotni almashtirdi (offline, pytest-asyncio tasdiqlangan). E'tibor bering: filtrlar bir-birini istisno qiladi (matn "orqaga" yoki raqam), shuning uchun ikki handler bir-biriga xalaqit bermaydi.
14-mashq.
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, Message, Chat, User
# ... Survey router import qilinadi ...
def upd(uid, text, user_id):
msg = Message(message_id=uid, date=datetime.now(),
chat=Chat(id=user_id, type="private"),
from_user=User(id=user_id, is_bot=False, first_name="U"),
text=text)
return Update(update_id=uid, message=msg)
async def main():
bot = Bot(token="123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
await dp.feed_update(bot, upd(1, "/start", 100)) # 100 -> name
await dp.feed_update(bot, upd(2, "/start", 200)) # 200 -> name
await dp.feed_update(bot, upd(3, "Oqil", 100)) # 100 -> age
# 100 endi age'da, 200 hali name'da
await bot.session.close()
asyncio.run(main())
get_state() ni har bir foydalanuvchi uchun tekshirsangiz: 100 -> "Survey:age", 200 -> "Survey:name". Holatlar aralashmadi — aiogram kalitni bot+chat+user bo'yicha ajratadi (offline tasdiqlangan).
15-mashq.
import pytest
# ... Survey router ...
SAVED = []
def save_to_db(data):
SAVED.append(dict(data)) # soxta "saqlash"
@router.message(Survey.city, F.text)
async def done(message: Message, state: FSMContext):
await state.update_data(city=message.text)
data = await state.get_data()
save_to_db(data)
await state.clear()
@pytest.mark.asyncio
async def test_full_flow():
bot = Bot(token="123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
for i, t in enumerate(["/start", "Oqil", "30", "Toshkent"], 1):
await dp.feed_update(bot, upd(i, t, 5))
await bot.session.close()
assert SAVED[-1] == {"name": "Oqil", "age": 30, "city": "Toshkent"}
Test ro'yxatda to'g'ri yozuv borligini tasdiqlaydi. state.clear() dan keyin get_state() None qaytaradi. (Bizning bobdagi shu naqsh pytest-asyncio bilan o'tdi.)
16-mashq.
from aiogram.filters import Command, StateFilter
@router.message(StateFilter("*"), Command("cancel"))
@router.message(StateFilter("*"), F.text.casefold() == "bekor")
async def cancel(message: Message, state: FSMContext):
if await state.get_state() is None:
await message.answer("Bekor qiladigan amal yo'q.")
return
await state.clear()
await message.answer("Bekor qilindi.")
Ikki dekorator bitta funksiyaga ulanadi — /cancel buyrug'i ham, "bekor"/"BEKOR"/"Bekor" matni ham (casefold registrga befarq) shu handlerni ishga tushiradi. Ikkalasi ham clear() chaqiradi (offline /cancel va "bekor" — ikki yo'l ham holatni tozalashi tasdiqlangan).
⬅️ Oldingi: 07 — Callback query va inline rejim · 🏠 README · Keyingi: 09 — Middleware ➡️