Модуль 4

Uvicorn і життєвий цикл FastAPI-застосунку

Ось готовий урок, створений спеціально за твоїм запитом у стилі CS50 та Девіда Малана.


🎓 Тема: Uvicorn і життєвий цикл FastAPI-застосунку

Привіт, друзі! 👋

Сьогодні ми зазирнемо під капот того, як насправді "живуть" наші веб-застосунки. Ви вже вмієте писати ендпоінти, які повертають JSON. Ви — як шеф-кухарі, що створили геніальне меню.

Але уявіть ситуацію: шеф-кухар стоїть на кухні з каструлею в руках, а ресторану... не існує. Немає офіціантів, немає вхідних дверей, немає навіть електрики.

Як клієнт потрапить до вас? Хто прийме замовлення і передасть його на кухню?

Саме тут на сцену виходить Uvicorn і поняття життєвого циклу (Lifecycle). Поїхали розбиратися! 🚀


1. 🔥 Вступ: проблема та мотивація

Уявіть, що ви відкриваєте кав'ярню. Кожен раз, коли клієнт заходить і замовляє каву, ви: 1. Купуєте кавомашину. 2. Підключаєте її до розетки. 3. Чекаєте, поки вона нагріється. 4. Робите каву. 5. ...і викидаєте кавомашину на смітник.

Звучить як божевілля, правда? Це дорого і неймовірно довго.

Але саме це роблять новачки у коді: вони намагаються підключатися до бази даних або завантажувати важку модель штучного інтелекту всередині кожного запиту користувача.

Питання до вас: Як зробити це правильно? Очевидно: ми хочемо підготувати кавомашину один раз вранці (коли відкриваємо заклад), користуватися нею весь день, і вимкнути її ввечері (коли закриваємось).

Це і є Життєвий цикл (Lifecycle). А Uvicorn — це той менеджер залу, який відкриває двері, вмикає світло і запускає ваш FastAPI-код. Без цього вашого коду просто не існує для зовнішнього світу.


2. 🧠 Теоретична база (без нудьги)

Давайте розкладемо все по поличках.

🔹 Що таке Uvicorn?

FastAPI сам по собі — це просто набір інструкцій (ваш код). Він не вміє слухати мережу. Uvicorn — це ASGI-сервер.

  • Аналогія: FastAPI — це мозок 🧠, а Uvicorn — це вуха і рот 👂👄.
  • Uvicorn слухає порт (наприклад, 8000), отримує сирий HTTP-запит, перетворює його на зручний Python-об'єкт і передає у FastAPI.
  • Коли FastAPI опрацював дані, Uvicorn бере відповідь і відправляє її назад клієнту.

Що треба запам'ятати: FastAPI створює додаток, Uvicorn його запускає.

🔹 Життєвий цикл (Lifespan)

Це період часу від запуску програми до її зупинки.

У сучасному FastAPI (і це Best Practice, яку використовують профі) ми використовуємо механізм lifespan через Context Manager.

Як це працює під капотом: 1. Uvicorn запускається. 2. Виконується код ДО слова yield (наприклад, підключення до БД). 3. Додаток починає приймати запити від користувачів (працює годинами, днями...). 4. Коли ви натискаєте Ctrl+C (зупинка), виконується код ПІСЛЯ слова yield (закриття з'єднань).


3. 🧪 Приклади

Приклад 1: Мінімальний запуск (Hello Uvicorn)

Створимо файл main.py.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Привіт, світе!"}

Ви часто бачили, що це не працює, якщо просто натиснути "Play" в редакторі. Чому? Бо ми описали шеф-кухаря, але не найняли офіціанта!

Запускаємо через термінал:

uvicorn main:app --reload
  • main — назва файлу.
  • app — назва змінної всередині файлу.
  • --reload — "гаряче" оновлення (якщо зміните код, сервер перезапуститься сам).

Приклад 2: Правильний Lifespan (Магія Context Manager)

А тепер найцікавіше. Давайте зімітуємо підключення до бази даних.

Що ви очікуєте побачити в консолі, якщо ми запустимо цей код? 👇

from contextlib import asynccontextmanager
from fastapi import FastAPI

# Фальшива база даних
fake_db = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Цей блок виконується ПЕРЕД запуском сервера
    print("🔋 ЗАПУСК: Підключаємось до бази даних...")
    fake_db["status"] = "Connected"

    yield  # <-- Тут сервер працює і приймає запити

    # Цей блок виконується ПІСЛЯ зупинки сервера
    print("🪫 ЗУПИНКА: Закриваємо з'єднання...")
    fake_db.clear()

app = FastAPI(lifespan=lifespan)

@app.get("/db-status")
async def get_status():
    return {"db": fake_db}

Розбір польотів: 1. Ви побачите повідомлення 🔋 ЗАПУСК... лише один раз! 2. Потім ви можете тисячу разів звертатися до /db-status — код підключення не буде виконуватися знову. Це супершвидко. 3. Коли ви натиснете Ctrl+C у терміналі, ви побачите 🪫 ЗУПИНКА.... Це гарантує, що дані не пошкодяться при вимкненні.


Приклад 3: Завантаження ML-моделі (Реальний кейс)

Уявіть, що у вас є модель машинного навчання, яка важить 2 ГБ.

Поганий шлях: Завантажувати її всередині функції predict(). Користувач чекатиме 10 секунд на відповідь. ✅ Шлях джедая: Завантажити її в lifespan.

from contextlib import asynccontextmanager
from fastapi import FastAPI
import time

ml_models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("🤖 Завантажуємо важку AI модель...")
    time.sleep(2) # Імітація важкої роботи
    ml_models["super_ai"] = lambda x: x * 2 # Наша "модель"
    print("✅ Модель готова до роботи!")

    yield

    print("🧹 Очищаємо пам'ять...")
    ml_models.clear()

app = FastAPI(lifespan=lifespan)

@app.get("/predict/{number}")
async def predict(number: int):
    # Модель вже в пам'яті! Відповідь миттєва.
    result = ml_models["super_ai"](number)
    return {"input": number, "prediction": result}

4. 🛠 Практична частина

Час забруднити руки кодом! Виконайте ці завдання:

  1. 🔹 Перший запуск: Створіть файл server.py, напишіть простий ендпоінт і запустіть його через Uvicorn на порту 8001 (підказка: прапор --port).
  2. 🔹 Логування: Додайте lifespan, який пише в консоль "СЕРВЕР ВСТАВ!" при старті і "СЕРВЕР ПІШОВ СПАТИ..." при вимкненні.
  3. 🔹 Глобальний кеш: Створіть глобальний словник cache = {}. У lifespan покладіть туди значення {"welcome_msg": "Ласкаво просимо!"}. Створіть ендпоінт, який повертає це повідомлення.
  4. 🔹 Виправ помилку: python # Студент написав це, але сервер падає або не запускає логіку. Чому? @asynccontextmanager def lifespan(app: FastAPI): # <-- Уважно тут print("Start") # <-- Чогось не вистачає? print("End")
  5. 🔹 Міні-кейс "Конфіг": Уявіть, що вам треба прочитати файл налаштувань config.txt (створіть його з будь-яким текстом) лише один раз при старті. Реалізуйте це через lifespan і зробіть ручку (endpoint), яка показує зміст конфігу.

5. 💡 Мислення як у розробника

Як відрізнити новачка від профі в цій темі?

❌ Новачок: Пише код підключення до бази даних (connect_db()) прямо всередині функції обробки запиту (def get_users():). Наслідок: Якщо у вас 1000 запитів на секунду, ваш сервер намагається відкрити 1000 з'єднань одночасно і "лягає".

✅ Профі (Ви): Розуміє, що ресурси (пам'ять, з'єднання з БД, сокети) — дорогі. Він ініціалізує їх один раз на старті (lifespan -> startup), використовує глобально (через app.state або dependency injection) і коректно закриває (lifespan -> shutdown), щоб не лишати "зомбі-з'єднань".

Порада з практики: Раніше у FastAPI використовували @app.on_event("startup"). Ви можете зустріти це в старому коді або туторіалах на YouTube. Знайте: це працює, але це застарілий метод. Використовуйте lifespan — це сучасний стандарт.


6. 🧩 Підсумок

Отже, що ми маємо в сухому залишку?

  1. FastAPI — це інструкція, Uvicorn — це виконавець.
  2. Ми не купуємо нову "кавомашину" для кожного клієнта. Ми використовуємо Lifespan.
  3. asynccontextmanager + yield — це наш інструмент для розділення логіки старту і фінішу.

Тепер ви вмієте: * Правильно запускати сервер. * Економити ресурси сервера, завантажуючи важкі дані лише один раз. * Граційно завершувати роботу програми.

👀 Що далі? Тепер, коли ми вміємо відкривати з'єднання на старті... наступного разу ми підключимо справжню SQL базу даних, щоб зберігати дані назавжди, навіть після перезавантаження Uvicorn!

Це був CS50... тобто, наш урок з FastAPI. 😉 Код написали? Запускайте!