Ось готовий урок, створений спеціально за твоїм запитом у стилі 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. 🛠 Практична частина
Час забруднити руки кодом! Виконайте ці завдання:
- 🔹 Перший запуск: Створіть файл
server.py, напишіть простий ендпоінт і запустіть його через Uvicorn на порту8001(підказка: прапор--port). - 🔹 Логування: Додайте
lifespan, який пише в консоль "СЕРВЕР ВСТАВ!" при старті і "СЕРВЕР ПІШОВ СПАТИ..." при вимкненні. - 🔹 Глобальний кеш: Створіть глобальний словник
cache = {}. Уlifespanпокладіть туди значення{"welcome_msg": "Ласкаво просимо!"}. Створіть ендпоінт, який повертає це повідомлення. - 🔹 Виправ помилку:
python # Студент написав це, але сервер падає або не запускає логіку. Чому? @asynccontextmanager def lifespan(app: FastAPI): # <-- Уважно тут print("Start") # <-- Чогось не вистачає? print("End") - 🔹 Міні-кейс "Конфіг": Уявіть, що вам треба прочитати файл налаштувань
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. 🧩 Підсумок
Отже, що ми маємо в сухому залишку?
- FastAPI — це інструкція, Uvicorn — це виконавець.
- Ми не купуємо нову "кавомашину" для кожного клієнта. Ми використовуємо Lifespan.
asynccontextmanager+yield— це наш інструмент для розділення логіки старту і фінішу.
Тепер ви вмієте: * Правильно запускати сервер. * Економити ресурси сервера, завантажуючи важкі дані лише один раз. * Граційно завершувати роботу програми.
👀 Що далі? Тепер, коли ми вміємо відкривати з'єднання на старті... наступного разу ми підключимо справжню SQL базу даних, щоб зберігати дані назавжди, навіть після перезавантаження Uvicorn!
Це був CS50... тобто, наш урок з FastAPI. 😉 Код написали? Запускайте!