Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.
Вопросы и ответы
37 подробных ответов
01Что такое WSGI и ASGI, и в чём разница?
middle
Короткий ответ: WSGI (Web Server Gateway Interface) — синхронный стандарт интерфейса между веб-сервером и Python-приложением. ASGI (Asynchronous Server Gateway Interface) — его асинхронный преемник, поддерживающий async/await, WebSocket и long-polling. WSGI = одно соединение блокирует воркер; ASGI = один воркер обслуживает много соединений конкурентно.
Подробно:
WSGI определён в PEP 3333. Приложение — это вызываемый объект (environ, start_response) -> iterable:
# WSGI-приложение (синхронное)
def application(environ, start_response):
status = "200 OK"
headers = [("Content-Type", "text/plain")]
start_response(status, headers)
return [b"Hello WSGI"]
Модель обработки строго синхронная: воркер берёт запрос, выполняет его целиком (включая блокирующие I/O — БД, HTTP к внешним сервисам), и только потом берёт следующий. Под WSGI работают Django (классически), Flask.
ASGI — это async def app(scope, receive, send). scope описывает соединение (тип: http, websocket, lifespan), receive/send — асинхронные каналы событий:
# ASGI-приложение (асинхронное)
async def application(scope, receive, send):
assert scope["type"] == "http"
await send({
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/plain")],
})
await send({
"type": "http.response.body",
"body": b"Hello ASGI",
})
Под ASGI работают FastAPI, Starlette, современный Django (с async def views), Channels.
| WSGI | ASGI | |
|---|---|---|
| Модель | синхронная | асинхронная (async/await) |
| Сигнатура | (environ, start_response) |
async (scope, receive, send) |
| WebSocket | нет | да |
| Конкурентность | через процессы/потоки | через event loop + потоки/процессы |
| Серверы | gunicorn, uWSGI | uvicorn, hypercorn, daphne |
| Фреймворки | Django, Flask | FastAPI, Starlette, async Django |
⚠️ Ловушка: ASGI не делает «обычный» (CPU-bound) код быстрее. Выигрыш только на I/O-bound задачах, где много времени тратится на ожидание. Если внутри async def view вызвать блокирующую функцию (например, синхронный драйвер БД или time.sleep), вы заблокируете весь event loop и убьёте конкурентность всех соединений на этом воркере.
02Зачем нужен ASGI, если WSGI годами всех устраивал?
concept
Короткий ответ: WSGI принципиально не поддерживает long-lived соединения (WebSocket, SSE) и асинхронный I/O. ASGI решает обе проблемы: позволяет одному воркеру держать тысячи открытых соединений и эффективно обслуживать I/O-bound нагрузку без раздувания числа процессов/потоков.
Подробно:
Проблемы WSGI:
- Один запрос = один занятый воркер. При 100 одновременных «медленных» запросах (например, ожидание ответа от внешнего API 2 сек) нужно ~100 воркеров/потоков. Каждый процесс ест память (десятки–сотни МБ).
- Нет WebSocket / SSE. Модель «запрос-ответ» завершается сразу, нет места долгоживущему соединению.
- Нет нативного async. Нельзя
awaitвнутри view.
С ASGI один event loop конкурентно обслуживает множество ожидающих I/O корутин:
# FastAPI: пока ждём внешний API, event loop обслуживает другие запросы
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/proxy")
async def proxy():
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com/data") # await => loop свободен
return r.json()
Когда выбирать что:
- CPU-bound, простой CRUD, мало конкурентных соединений → WSGI вполне достаточно.
- Много медленного I/O, real-time (чаты, нотификации), стриминг, высокая конкурентность → ASGI.
⚠️ Ловушка: ASGI — не «серебряная пуля». Под ним легко получить деградацию, если случайно вызвать синхронный блокирующий код. В Django под ASGI ORM остаётся синхронным — обращения к БД оборачиваются в sync_to_async (через thread pool), и неаккуратное использование сводит на нет выигрыш.
03Чем отличаются gunicorn и uvicorn? Как их комбинируют?
middle
Короткий ответ: gunicorn — production WSGI-сервер с менеджером процессов-воркеров (pre-fork). uvicorn — лёгкий ASGI-сервер на uvloop/httptools. На практике часто запускают gunicorn как менеджер процессов с классом воркера uvicorn.workers.UvicornWorker, чтобы получить и надёжное управление процессами, и ASGI.
Подробно:
# Чистый WSGI (Django/Flask)
gunicorn myproject.wsgi:application --workers 5 --bind 0.0.0.0:8000
# Чистый ASGI (FastAPI) — uvicorn напрямую
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
# Лучшее из двух миров: gunicorn управляет, uvicorn обрабатывает ASGI
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
Зачем gunicorn перед uvicorn: gunicorn даёт зрелое управление процессами — graceful reload, перезапуск зависших воркеров (--timeout), мониторинг. uvicorn-воркеры дают async-обработку.
Типы воркеров gunicorn (WSGI):
sync(по умолчанию) — 1 запрос на воркер, надёжно, для CPU-bound.gthread— потоки внутри воркера (--threads N), для смешанной нагрузки.gevent/eventlet— green threads, для I/O-bound на WSGI.
⚠️ Ловушка: Не запускайте uvicorn в --reload режиме в production — он для разработки и медленнее. Также не путайте --workers (процессы) с --threads (потоки внутри процесса): у async-воркеров потоки обычно не нужны, конкурентность даёт event loop.
04Что такое MTV в Django и как это соотносится с MVC?
junior
Короткий ответ: Django использует паттерн MTV — Model (данные/ORM), Template (представление/HTML), View (бизнес-логика, решает что показать). Это тот же MVC, но «Controller» в Django — это сам фреймворк (URL dispatcher), а «View» в MVC соответствует «Template» в Django.
Подробно:
| MVC | Django MTV | Роль |
|---|---|---|
| Model | Model | Данные, ORM, бизнес-сущности |
| View | Template | Презентация (HTML/JSON) |
| Controller | View | Логика обработки запроса |
| — | (URLconf) | Маршрутизация (роль «контроллера» MVC) |
# Model — models.py
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
# View — views.py (это «контроллер» в терминах MVC)
from django.shortcuts import render
def article_detail(request, pk):
article = Article.objects.get(pk=pk)
return render(request, "article.html", {"article": article})
# Template — article.html
# <h1>{{ article.title }}</h1><p>{{ article.body }}</p>
⚠️ Ловушка: Частая путаница на собеседовании: в Django «View» — это НЕ то, что видит пользователь (как в MVC), а обработчик запроса. То, что видит пользователь, — это Template.
05Опиши путь запроса в Django от сервера до ответа.
middle
Короткий ответ: WSGI/ASGI сервер → WSGI/ASGI handler Django → стек middleware (вниз) → URL resolver → view → (ORM/template) → HttpResponse → стек middleware (вверх) → сервер → клиент.
Подробно:
- Сервер (gunicorn) получает HTTP-запрос, формирует
environ, вызываетdjango.core.wsgi.get_wsgi_application(). - Django создаёт объект
HttpRequest. - Запрос проходит вниз через
MIDDLEWARE(фаза «request» каждого middleware). URLResolverсопоставляет path сurlpatterns, находит view.- View выполняет логику: запросы к ORM, рендер шаблона, формирование данных.
- View возвращает
HttpResponse. - Ответ проходит вверх через middleware в обратном порядке (фаза «response»).
- Сервер отдаёт ответ клиенту.
def my_view(request):
# request — это HttpRequest
name = request.GET.get("name", "World")
return HttpResponse(f"Hello, {name}") # это HttpResponse
⚠️ Ловушка: Если middleware вернёт response раньше (например, редирект на логин в auth middleware), нижележащие middleware и view вообще не выполнятся — это короткое замыкание. Важно понимать, на какой стадии каждый middleware прерывает цепочку.
06Как работает маршрутизация URL в Django?
junior
Короткий ответ: Список urlpatterns сопоставляет URL-паттерны с views. Используются path() (с конвертерами типов) и re_path() (regex). URLconf можно вкладывать через include() для модульности приложений.
Подробно:
# project/urls.py
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")), # делегируем приложению
]
# blog/urls.py
from django.urls import path, re_path
from . import views
urlpatterns = [
path("", views.index, name="blog-index"),
path("<int:pk>/", views.detail, name="blog-detail"), # конвертер int
path("<slug:slug>/", views.by_slug), # конвертер slug
re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive),
]
name= позволяет реверсировать URL без хардкода:
from django.urls import reverse
url = reverse("blog-detail", kwargs={"pk": 42}) # "/blog/42/"
# В шаблоне: {% url 'blog-detail' pk=42 %}
⚠️ Ловушка: Порядок паттернов важен — Django берёт первое совпадение. <slug:slug> поставленный раньше <int:pk>/ может перехватить запросы. Также не забывайте слеши: настройка APPEND_SLASH делает редирект, но только для GET, и сломает POST без завершающего слеша.
07Function-Based Views vs Class-Based Views — что выбрать?
middle
Короткий ответ: FBV (function-based) — простые функции (request) -> response, явные и читаемые. CBV (class-based) — классы с методами get/post, дают переиспользование через наследование и mixins, generic views сокращают шаблонный код для CRUD.
Подробно:
# FBV
from django.shortcuts import render, get_object_or_404
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, "detail.html", {"article": article})
# CBV — generic
from django.views.generic import DetailView, ListView
class ArticleDetail(DetailView):
model = Article
template_name = "detail.html"
class ArticleList(ListView):
model = Article
paginate_by = 20
# urls.py: path("<int:pk>/", ArticleDetail.as_view())
- FBV плюсы: простота, явность, легко читать; декораторы (
@login_required) очевидны. - CBV плюсы: DRY для типового CRUD, mixins (
LoginRequiredMixin,PermissionRequiredMixin), переопределение методов (get_queryset,get_context_data). - CBV минусы: «магия» наследования, сложно проследить, откуда метод (MRO).
⚠️ Ловушка: В CBV декораторы вешаются не напрямую, а через method_decorator или mixins. @login_required def get(...) на методе не сработает как ожидается — нужно @method_decorator(login_required, name="dispatch") на классе или LoginRequiredMixin.
08Как устроен middleware в Django и почему важен порядок?
middle
Короткий ответ: Middleware — это слои-обёртки вокруг view, обрабатывающие каждый request/response. Они вызываются вниз по списку MIDDLEWARE на входе и вверх (в обратном порядке) на выходе. Современный стиль — callable-класс, оборачивающий get_response.
Подробно:
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # вызывается один раз при старте
def __call__(self, request):
# --- код ДО view (фаза request, порядок сверху вниз) ---
import time
start = time.monotonic()
response = self.get_response(request) # вызов следующего слоя / view
# --- код ПОСЛЕ view (фаза response, порядок снизу вверх) ---
response["X-Elapsed-Ms"] = int((time.monotonic() - start) * 1000)
return response
Дополнительные хуки: process_view, process_exception, process_template_response.
Типичный порядок (важно!):
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", # до auth
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", # нужен session
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
⚠️ Ловушка: AuthenticationMiddleware зависит от SessionMiddleware — если поставить выше, request.user будет недоступен. Порядок — это и есть контракт зависимостей между слоями.
09Объясни концепцию middleware и почему порядок имеет значение.
concept
Короткий ответ: Middleware — это цепочка обёрток (паттерн «луковица»/decorator chain) вокруг бизнес-логики, реализующая сквозную функциональность (логирование, auth, CORS, сжатие, безопасность). Порядок важен, потому что request идёт сквозь слои в одном направлении, а response — в обратном, и слои зависят от данных, выставленных предыдущими.
Подробно:
Модель «луковицы» (onion): запрос «проникает» внутрь через слои, view в центре, ответ «выходит» через те же слои в обратном порядке.
request → [Security] → [Session] → [Auth] → [CSRF] → VIEW
response ← [Security] ← [Session] ← [Auth] ← [CSRF] ← VIEW
Это концепция, общая для всех фреймворков:
- Django: список
MIDDLEWARE. - FastAPI/Starlette:
app.add_middleware(...)— добавленные позже оборачивают добавленных раньше (внешний слой = последний добавленный). - Flask: WSGI middleware +
before_request/after_requestхуки.
Почему порядок критичен:
- Auth middleware должен идти после session middleware (читает сессию).
- GZip middleware должен сжимать уже готовый ответ — обычно ближе к внешнему слою.
- CORS должен отрабатывать рано, чтобы preflight-запросы не доходили до тяжёлой логики.
⚠️ Ловушка: В FastAPI/Starlette порядок инверсный относительно интуиции: middleware, добавленный последним через add_middleware, выполняется ПЕРВЫМ для запроса (он самый внешний). Легко перепутать с Django, где первый в списке — самый внешний.
10Как организованы settings и apps в Django?
junior
Короткий ответ: settings.py — единый модуль конфигурации (БД, INSTALLED_APPS, middleware, секреты). App — самодостаточный модуль функциональности (models.py, views.py, migrations/), регистрируемый в INSTALLED_APPS. Проект состоит из множества apps.
Подробно:
# settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"rest_framework", # сторонние
"blog.apps.BlogConfig", # своё приложение
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mydb",
"HOST": os.environ["DB_HOST"],
}
}
SECRET_KEY = os.environ["SECRET_KEY"] # из env, не хардкод!
DEBUG = os.environ.get("DEBUG", "0") == "1"
Для разных окружений — разбивают settings: base.py, dev.py, prod.py и указывают DJANGO_SETTINGS_MODULE.
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
import blog.signals # регистрация сигналов
⚠️ Ловушка: DEBUG = True в production — серьёзная уязвимость: показывает stack traces с переменными окружения, и ALLOWED_HOSTS игнорируется. Также SECRET_KEY в репозитории = компрометация сессий и токенов.
11Объясни миграции и ленивость QuerySet в Django ORM.
middle
Короткий ответ: Миграции — версионируемые Python-файлы, описывающие изменения схемы БД (makemigrations генерирует, migrate применяет). QuerySet ленивый — SQL не выполняется при создании, только при первом обращении к данным (итерация, len, slice, list(), bool).
Подробно:
# Миграции
# python manage.py makemigrations -> создаёт 0002_xxx.py
# python manage.py migrate -> применяет к БД
# python manage.py showmigrations -> статус
# Ленивость
qs = Article.objects.filter(published=True) # SQL НЕ выполнен
qs = qs.exclude(draft=True) # всё ещё НЕ выполнен (chaining)
qs = qs.order_by("-created") # НЕ выполнен
for a in qs: # <-- ВОТ ТУТ выполняется SQL (evaluation)
print(a.title)
print(len(qs)) # уже закэшировано, нового запроса нет
Что триггерит выполнение: итерация, list(qs), len(qs), bool(qs), slicing с шагом, repr().
select_related загружает ForeignKey/OneToOne через SQL JOIN, а prefetch_related выполняет второй запрос для many-to-many и обратных связей и объединяет результаты в Python. Индексы должны соответствовать реальным фильтрам и сортировкам QuerySet; transaction.atomic() задаёт явную транзакционную границу для нескольких зависимых записей.
⚠️ Ловушка: qs.count() делает SELECT COUNT(*), а len(qs) загружает ВСЕ объекты в память и считает их. Для проверки «есть ли записи» используйте qs.exists() (эффективный SELECT 1 ... LIMIT 1), а не if qs: или len(qs).
12Как бороться с N+1 запросами в Django?
middle
Короткий ответ: select_related — для ForeignKey/OneToOne, делает SQL JOIN (один запрос). prefetch_related — для ManyToMany/обратных FK, делает отдельный запрос и джойнит в Python. Оба устраняют проблему N+1.
Подробно:
# N+1 ПРОБЛЕМА: 1 запрос на статьи + N запросов на авторов
for article in Article.objects.all():
print(article.author.name) # отдельный SQL на каждой итерации
# select_related — FK/O2O, через JOIN
for article in Article.objects.select_related("author"):
print(article.author.name) # всё в одном запросе
# prefetch_related — M2M / обратные связи
for article in Article.objects.prefetch_related("tags"):
print([t.name for t in article.tags.all()]) # 2 запроса всего
Подробный разбор (когда что, Prefetch-объекты, only/defer, аннотации) — в файле про Django ORM в этой же директории.
⚠️ Ловушка: select_related по M2M невозможен (нельзя JOIN-нуть множество строк в одну) — для M2M только prefetch_related. Чрезмерный select_related с глубокими цепочками порождает огромные JOIN и дублирование данных.
13Как работают формы и валидация в Django?
middle
Короткий ответ: forms.Form/forms.ModelForm декларативно описывают поля, валидируют входные данные (is_valid()), очищают их (cleaned_data) и рендерят HTML. ModelForm автоматически строит поля из модели. Валидация — на уровне поля (clean_<field>) и формы (clean).
Подробно:
from django import forms
class ContactForm(forms.Form):
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, max_length=2000)
def clean_message(self): # валидация одного поля
data = self.cleaned_data["message"]
if "spam" in data.lower():
raise forms.ValidationError("No spam allowed")
return data
def clean(self): # кросс-полевая валидация
cleaned = super().clean()
# ... сравнение нескольких полей
return cleaned
# ModelForm — поля из модели
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "body"]
# во view
def view(request):
form = ContactForm(request.POST or None)
if request.method == "POST" and form.is_valid():
send_email(form.cleaned_data) # данные уже очищены и типизированы
⚠️ Ловушка: Обращаться к form.cleaned_data до вызова form.is_valid() нельзя — атрибута ещё нет. И в clean_<field> обязательно return значение — иначе поле станет None в cleaned_data.
14Что такое Django admin и как его кастомизировать?
junior
Короткий ответ: Django admin — автогенерируемая CRUD-панель для моделей, построенная по INSTALLED_APPS. Модели регистрируются через admin.site.register или декоратор @admin.register, поведение настраивается через ModelAdmin (списки, фильтры, поиск, inline).
Подробно:
from django.contrib import admin
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ("title", "author", "published", "created")
list_filter = ("published", "created")
search_fields = ("title", "body")
raw_id_fields = ("author",) # вместо тяжёлого dropdown
inlines = [CommentInline]
readonly_fields = ("created",)
Admin отлично подходит для внутренних инструментов и работы контент-менеджеров.
⚠️ Ловушка: Admin — это полноценный доступ к данным с правами на запись/удаление. Не давайте админ-доступ кому попало и не выставляйте /admin/ без ограничений (rate-limit, нестандартный путь, 2FA). Также list_display с методом, дёргающим связанную модель, легко создаёт N+1 — используйте list_select_related.
15Что такое signals в Django и почему с ними надо быть осторожным?
senior
Короткий ответ: Signals — механизм publish/subscribe: отправитель шлёт сигнал (post_save, pre_delete, m2m_changed), а подписчики-ресиверы реагируют. Удобно для слабой связанности, но опасно: скрытая логика, проблемы с порядком, сложность отладки и тестирования.
Подробно:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Почему осторожно:
- Неявность. Логика выполняется «магически» при сохранении — не видно из кода, который вызвал
save(). Тяжело отлаживать. - Порядок и каскад. Несколько ресиверов на один сигнал, сигнал внутри ресивера → каскад, бесконечные циклы.
bulk_create/update()не шлют сигналы.Model.objects.update(...)иbulk_createНЕ триггерятpost_save— частый источник багов.- Транзакции. Ресивер на
post_saveвыполняется внутри транзакции; если он шлёт письмо/ставит Celery-задачу, а транзакция откатится — будет рассинхрон. Используйтеtransaction.on_commit.
Альтернатива: явный вызов метода/сервиса вместо сигнала, когда логика однозначна.
⚠️ Ловушка: Profile.objects.create() внутри post_save для User снова триггерит post_save (для Profile) — следите за бесконечной рекурсией. И помните: queryset.update() обходит и save(), и сигналы.
16Какие механизмы безопасности Django даёт из коробки?
middle
Короткий ответ: Django по умолчанию защищает от CSRF (токены в формах), XSS (автоэкранирование в шаблонах), SQL-инъекций (параметризованные запросы ORM), clickjacking (X-Frame-Options), и поддерживает безопасное хранение паролей (PBKDF2/Argon2), HTTPS-настройки.
Подробно:
# CSRF: токен обязателен для POST-форм
# в шаблоне: <form method="post">{% csrf_token %} ... </form>
# CsrfViewMiddleware проверяет токен; иначе 403
# XSS: автоэкранирование в Django Templates
# {{ user_input }} -> < превращается в < автоматически
# Отключение опасно: {{ user_input|safe }} или mark_safe()
# SQL injection: ORM параметризует запросы
Article.objects.filter(title=user_input) # безопасно (placeholder)
Article.objects.raw("SELECT * FROM a WHERE t=%s", [user_input]) # безопасно
# ОПАСНО: .raw(f"... WHERE t='{user_input}'") — инъекция!
# Clickjacking
MIDDLEWARE += ["django.middleware.clickjacking.XFrameOptionsMiddleware"]
X_FRAME_OPTIONS = "DENY"
# Production-настройки
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
⚠️ Ловушка: Фильтр |safe и mark_safe() отключают XSS-защиту — никогда не применяйте к пользовательскому вводу. И raw()/extra()/.raw() с f-строками сводят на нет защиту от SQL-инъекций — всегда передавайте параметры списком, а не интерполяцией.
17В чём разница между Serializer и ModelSerializer в DRF?
middle
Короткий ответ: Serializer — ручное декларативное описание полей и (де)сериализации/валидации, полный контроль. ModelSerializer — генерирует поля автоматически из модели и даёт готовые create/update, экономя код для типового CRUD.
Подробно:
from rest_framework import serializers
# Ручной Serializer
class ArticleSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
body = serializers.CharField()
def create(self, validated_data):
return Article.objects.create(**validated_data)
def validate_title(self, value): # валидация поля
if Article.objects.filter(title=value).exists():
raise serializers.ValidationError("Title must be unique")
return value
# ModelSerializer — поля и create/update автоматически
class ArticleModelSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.name", read_only=True)
class Meta:
model = Article
fields = ["id", "title", "body", "author_name"]
read_only_fields = ["id"]
Сериализатор работает в обе стороны: serializer.data (объект → dict/JSON) и serializer.is_valid() + serializer.validated_data (входной dict → проверенные данные).
⚠️ Ловушка: fields = "__all__" в ModelSerializer — анти-паттерн для API: при добавлении поля в модель оно автоматически утечёт в API (например, is_admin, password_hash). Указывайте поля явно. Также вложенные сериализаторы по умолчанию read-only — для записи нужен кастомный create/update.
18Что такое ViewSet и router в DRF?
middle
Короткий ответ: ViewSet группирует логику для набора связанных эндпоинтов (list/create/retrieve/update/destroy) в одном классе. Router автоматически генерирует URL-маршруты под этот ViewSet, избавляя от ручного описания urlpatterns.
Подробно:
from rest_framework import viewsets
from rest_framework.routers import DefaultRouter
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet): # CRUD из коробки
queryset = Article.objects.select_related("author")
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
@action(detail=True, methods=["post"]) # доп. эндпоинт /articles/{pk}/publish/
def publish(self, request, pk=None):
article = self.get_object()
article.published = True
article.save()
return Response({"status": "published"})
# urls.py
router = DefaultRouter()
router.register(r"articles", ArticleViewSet) # генерирует все маршруты
urlpatterns = router.urls
Иерархия: APIView (база) → GenericAPIView + mixins → generic views → ViewSet/ModelViewSet.
⚠️ Ловушка: ModelViewSet даёт сразу все операции, включая DELETE и PUT/PATCH. Если нужны только чтение — используйте ReadOnlyModelViewSet или ограничьте http_method_names, иначе случайно откроете запись/удаление.
19Как устроены authentication и permission classes в DRF?
middle
Короткий ответ: Authentication classes определяют, КТО делает запрос (заполняют request.user). Permission classes определяют, МОЖНО ли ему выполнить действие. Сначала проходит аутентификация, потом проверка прав. Оба настраиваются глобально или на уровне view.
Подробно:
# settings.py — глобально
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# на уровне view
from rest_framework.permissions import IsAuthenticated, BasePermission
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.author == request.user
class ArticleViewSet(viewsets.ModelViewSet):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated, IsOwner]
Готовые permissions: AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly, DjangoModelPermissions.
Два уровня проверки прав: has_permission (на весь запрос) и has_object_permission (на конкретный объект, вызывается из get_object).
⚠️ Ловушка: has_object_permission НЕ вызывается автоматически для list-эндпоинтов и для объектов, которые вы не получаете через get_object() — для list нужно фильтровать queryset вручную (get_queryset). Иначе пользователь увидит чужие записи в списке, хотя по объекту прав нет.
20Как настроить пагинацию и rate limiting в DRF?
middle
Короткий ответ: Pagination разбивает большие списки на страницы (PageNumberPagination, LimitOffsetPagination, CursorPagination). Throttling ограничивает частоту запросов (по пользователю/анониму/scope) для защиты от злоупотреблений.
Подробно:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/day",
"user": "1000/day",
"uploads": "10/min", # scoped throttle
},
}
# scoped throttle на конкретном view
class UploadView(APIView):
throttle_scope = "uploads"
Типы пагинации:
PageNumberPagination—?page=2, просто, ноOFFSETмедленный на больших данных.LimitOffsetPagination—?limit=20&offset=40.CursorPagination— стабильна при вставках, эффективна на больших таблицах (по индексированному полю).
⚠️ Ловушка: PageNumberPagination/LimitOffsetPagination используют SQL OFFSET, который на больших таблицах медленный (БД сканирует и отбрасывает пропускаемые строки) и нестабилен при вставках/удалениях (сдвиг страниц). Для больших объёмов и инфинит-скролла — CursorPagination.
21На чём построен FastAPI и почему он async-first?
middle
Короткий ответ: FastAPI = Starlette (ASGI-фреймворк: роутинг, middleware, WebSocket) + Pydantic (валидация и сериализация через type hints). Async-first, потому что Starlette построен на ASGI; type hints дают валидацию, документацию и автокомплит «бесплатно».
Подробно:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel): # Pydantic — валидация по type hints
name: str
price: float
in_stock: bool = True
@app.post("/items/")
async def create_item(item: Item) -> Item: # async + типизированный body
return item
Что даёт стек:
- Starlette — ASGI-маршрутизация, middleware, фоновые задачи, WebSocket, тестовый клиент.
- Pydantic — парсинг/валидация входа, сериализация выхода, генерация JSON Schema → OpenAPI.
Поддерживаются и def, и async def views: синхронные функции FastAPI выполняет в отдельном thread pool, чтобы не блокировать event loop.
⚠️ Ловушка: Если объявить async def, но внутри вызвать блокирующую операцию (синхронный SQLAlchemy, requests, time.sleep), вы заблокируете event loop. Либо используйте async-библиотеки (httpx, async-драйвер БД), либо объявляйте view как обычный def (тогда FastAPI сам уведёт его в thread pool).
22Как FastAPI различает path, query и body параметры? Что такое response_model?
middle
Короткий ответ: FastAPI выводит источник параметра из его типа и сигнатуры: переменная из пути → path param; простой тип не в пути → query param; Pydantic-модель → request body. response_model задаёт схему ответа: валидирует, фильтрует и документирует выход.
Подробно:
from fastapi import FastAPI, Query, Path
from pydantic import BaseModel
app = FastAPI()
class ItemIn(BaseModel):
name: str
password: str
class ItemOut(BaseModel): # без password — он отфильтруется
name: str
@app.get("/items/{item_id}")
async def read_item(
item_id: int = Path(..., gt=0), # path param, > 0
q: str | None = Query(None, max_length=50), # query param ?q=
limit: int = 10, # query param с дефолтом
):
return {"item_id": item_id, "q": q, "limit": limit}
@app.post("/items/", response_model=ItemOut) # ответ ограничен схемой ItemOut
async def create(item: ItemIn):
save(item)
return item # password в ответ НЕ попадёт — отфильтрован response_model
response_model — мощный инструмент безопасности: даже если вернуть лишние поля, в ответ попадут только описанные в модели.
⚠️ Ловушка: Без response_model (или -> ReturnType) FastAPI вернёт всё, что вернула функция, включая чувствительные поля (как is_admin, хэш пароля). Всегда задавайте явную выходную модель для эндпоинтов, отдающих данные.
23Как работает Dependency Injection через Depends в FastAPI?
middle
Короткий ответ: Depends объявляет зависимость — функцию/класс, которую FastAPI вызовет и результат подставит в параметр. Зависимости могут быть вложенными, кэшируются в рамках запроса, поддерживают yield для setup/teardown (например, сессия БД).
Подробно:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
# зависимость с teardown через yield
def get_db():
db = SessionLocal()
try:
yield db # значение подставляется в параметр
finally:
db.close() # выполнится после ответа
# зависимость, использующая другую зависимость
def get_current_user(db: Session = Depends(get_db), token: str = Header(...)):
user = db.query(User).filter_by(token=token).first()
if not user:
raise HTTPException(status_code=401)
return user
@app.get("/me")
async def me(user: User = Depends(get_current_user)):
return user
# зависимость уровня всего роутера / приложения
app = FastAPI(dependencies=[Depends(verify_api_key)])
Преимущества: переиспользование, тестируемость (через app.dependency_overrides), декларативность, автоматическое отображение в OpenAPI.
⚠️ Ловушка: Зависимость кэшируется в рамках ОДНОГО запроса (если одна и та же Depends встречается несколько раз, функция вызовется один раз). Чтобы отключить кэш — Depends(func, use_cache=False). Также yield-часть после yield выполняется уже после отправки ответа — не рассчитывайте там менять response.
24Объясни идею Dependency Injection и зачем она нужна.
concept
Короткий ответ: DI — это передача компоненту его зависимостей извне, а не создание их внутри. Это снижает связанность, упрощает замену реализаций и тестирование (можно подменить зависимость моком). FastAPI делает DI декларативным через Depends.
Подробно:
# БЕЗ DI: зависимость зашита внутри — тяжело тестировать
def get_user():
db = ProductionDatabase() # жёсткая связь
return db.fetch_user()
# С DI: зависимость передаётся снаружи
def get_user(db: Database = Depends(get_db)):
return db.fetch_user()
# В тесте подменяем реальную БД на фейк
app.dependency_overrides[get_db] = lambda: FakeDatabase()
Зачем:
- Тестируемость — подмена реальных сервисов моками.
- Слабая связанность — код зависит от абстракции, не от конкретной реализации.
- Переиспользование — общая логика (auth, БД-сессия, пагинация) выносится в зависимость.
- Управление жизненным циклом —
yieldоткрывает/закрывает ресурсы.
Аналоги в других фреймворках: Django делает это менее явно (settings, middleware, request.user); Flask — через flask.g и current_app.
⚠️ Ловушка: DI ≠ глобальный singleton. Распространённая ошибка — создавать тяжёлый объект (например, HTTP-клиент) в зависимости на каждый запрос вместо переиспользования. Для дорогих ресурсов используйте lifespan-события или модульный singleton, а через Depends отдавайте уже созданный объект.
25Как FastAPI генерирует документацию?
junior
Короткий ответ: Из type hints и Pydantic-моделей FastAPI автоматически строит OpenAPI-схему (JSON Schema). По ней доступны интерактивные UI: Swagger UI (/docs) и ReDoc (/redoc), а также сырой /openapi.json.
Подробно:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(title="My API", version="1.0.0")
class Item(BaseModel):
name: str = Field(..., description="Название товара", examples=["Книга"])
price: float = Field(..., gt=0, description="Цена > 0")
@app.post("/items/", summary="Создать товар", tags=["items"])
async def create(item: Item):
"""Создаёт новый товар. Текст docstring попадает в описание эндпоинта."""
return item
Доступно автоматически:
/docs— Swagger UI (интерактивный, можно слать запросы)./redoc— ReDoc (чистая документация)./openapi.json— спецификация для генерации клиентов, импорта в Postman и т.п.
⚠️ Ловушка: В production интерактивные доки часто отключают или закрывают аутентификацией (docs_url=None, redoc_url=None), чтобы не раскрывать структуру API. Также Swagger показывает примеры запросов — не оставляйте в examples реальные секреты/токены.
26Что такое BackgroundTasks и когда их использовать?
middle
Короткий ответ: BackgroundTasks позволяет выполнить функцию ПОСЛЕ отправки ответа клиенту, не блокируя его (отправка письма, лог, инвалидация кэша). Это легковесная альтернатива Celery для коротких задач, не критичных к надёжности.
Подробно:
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_log(message: str):
with open("log.txt", "a") as f:
f.write(message + "\n")
@app.post("/send/")
async def send(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_log, f"sent to {email}")
return {"status": "queued"} # ответ уходит сразу, лог пишется после
Когда BackgroundTasks, а когда Celery:
- BackgroundTasks — короткие, in-process, не критичные задачи. Выполняются в том же процессе; если процесс упадёт — задача потеряется.
- Celery — долгие, тяжёлые, требующие надёжности/ретраев/распределения по воркерам.
⚠️ Ловушка: BackgroundTasks выполняются в том же процессе/воркере — тяжёлая задача (CPU-bound, минутный экспорт) заблокирует воркер и съест его ресурсы. Для надёжности (гарантия выполнения, ретраи) и тяжёлых задач — только полноценная очередь (Celery/RQ/ARQ).
27За счёт чего FastAPI считается быстрым?
concept
Короткий ответ: За счёт ASGI + async I/O (один воркер держит много конкурентных запросов), быстрого ASGI-сервера (uvicorn на uvloop/httptools, частично на C), и валидации Pydantic v2 (ядро на Rust). FastAPI близок к производительности Node.js/Go на I/O-bound нагрузке.
Подробно:
Слагаемые скорости:
- ASGI + asyncio — один event loop конкурентно обслуживает множество I/O-bound запросов, пока они ждут сеть или БД; синхронный блокирующий код в
async defуничтожает это преимущество. - uvicorn / uvloop — uvloop — это замена asyncio event loop на базе libuv (того же, что в Node.js), httptools — быстрый HTTP-парсер на C.
- Pydantic v2 — валидационное ядро
pydantic-coreнаписано на Rust, в разы быстрее v1 (чистый Python). - Минимальные накладные расходы — Starlette тонкий, нет тяжёлого ORM/admin в ядре.
# Конкурентность: пока ждём 3 внешних запроса, loop их совмещает
import asyncio, httpx
@app.get("/aggregate")
async def aggregate():
async with httpx.AsyncClient() as c:
a, b, d = await asyncio.gather( # параллельно, не последовательно
c.get("https://s1/"), c.get("https://s2/"), c.get("https://s3/"),
)
return {"s1": a.json(), "s2": b.json(), "s3": d.json()}
⚠️ Ловушка: «FastAPI быстрый» относится к I/O-bound сценариям. На чистом CPU-bound коде Python остаётся Python (GIL), и async не помогает — для CPU-нагрузки нужны процессы (несколько воркеров) или вынос в Celery/процесс-пул. Сравнения «FastAPI vs Flask» по голому «hello world» вводят в заблуждение.
28Что такое Pydantic и чем v2 отличается от v1?
middle
Короткий ответ: Pydantic — библиотека валидации/сериализации данных по Python type hints через BaseModel. v2 переписана с ядром на Rust (pydantic-core) — в разы быстрее, с новым API валидаторов (field_validator, model_validator) и отдельным пакетом pydantic-settings для конфигурации.
Подробно:
from pydantic import BaseModel, field_validator, model_validator, EmailStr, Field
class User(BaseModel):
name: str = Field(..., min_length=1)
email: EmailStr
age: int = Field(..., ge=0, le=120)
@field_validator("name") # v2 (в v1 было @validator)
@classmethod
def name_titlecase(cls, v: str) -> str:
return v.title()
@model_validator(mode="after") # кросс-полевая валидация
def check(self):
if self.age < 18 and self.name == "admin":
raise ValueError("admin must be adult")
return self
u = User(name="ann", email="a@b.com", age="30") # "30" приводится к int 30
# u.name == "Ann"
Ключевые отличия v1 → v2:
- Ядро на Rust → производительность.
@validator→@field_validator,@root_validator→@model_validator..dict()→.model_dump(),.json()→.model_dump_json(),.parse_obj()→.model_validate().class Config→model_config = ConfigDict(...).- Settings вынесены в отдельный пакет
pydantic-settings.
# Конфигурация приложения из env
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_url: str
debug: bool = False
model_config = {"env_file": ".env"}
settings = Settings() # читает из переменных окружения / .env
⚠️ Ловушка: В v2 BaseSettings переехал из pydantic в pydantic_settings — частая ошибка миграции (ImportError). И помните, что v2 по умолчанию делает «мягкое» приведение типов ("30" → 30); если нужна строгость — Field(strict=True) или StrictInt.
29Что такое Flask, и что такое app/request context и blueprints?
middle
Короткий ответ: Flask — микрофреймворк (WSGI) на базе Werkzeug (HTTP/маршрутизация) и Jinja2 (шаблоны). Даёт минимум из коробки, остальное — через расширения. Blueprints модуляризуют приложение. Application context и request context — механизмы доступа к глобальным объектам (current_app, request, g) в рамках запроса.
Подробно:
from flask import Flask, Blueprint, request, g, current_app, render_template
app = Flask(__name__)
# Blueprint — модуль приложения
bp = Blueprint("blog", __name__, url_prefix="/blog")
@bp.route("/<int:post_id>")
def post(post_id):
return render_template("post.html", post_id=post_id) # Jinja2
app.register_blueprint(bp)
@app.before_request
def load_user():
g.user = get_user_from_session() # g — per-request хранилище
Контексты Flask:
- Application context —
current_app,g. Активен на время обработки запроса (или вручную черезwith app.app_context()). - Request context —
request,session. Доступ к данным текущего запроса.
Это «магические» прокси-объекты (через werkzeug.local), привязанные к текущему контексту (раньше — потоку, теперь — contextvars).
Расширения: Flask-SQLAlchemy (ORM), Flask-Migrate, Flask-Login, Flask-RESTful, Marshmallow (сериализация).
⚠️ Ловушка: Обращение к request/current_app вне контекста запроса даёт RuntimeError: Working outside of application/request context — частая проблема в фоновых задачах и скриптах. Решение: обернуть в with app.app_context(): или app.test_request_context().
30Как обрабатывать исключения во фреймворках?
middle
Короткий ответ: Каждый фреймворк даёт способ перехватить исключение и вернуть структурированный ответ: FastAPI — @app.exception_handler + HTTPException; DRF — кастомный EXCEPTION_HANDLER/APIException; Flask — @app.errorhandler; Django — process_exception middleware и кастомные handler404/500.
Подробно:
# FastAPI
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/items/{id}")
async def get(id: int):
if id == 0:
raise HTTPException(status_code=404, detail="Not found")
class TooManyError(Exception): ...
@app.exception_handler(TooManyError) # глобальный обработчик типа
async def handler(request: Request, exc: TooManyError):
return JSONResponse(status_code=429, content={"error": "slow down"})
# Flask
@app.errorhandler(404)
def not_found(e):
return {"error": "not found"}, 404
# DRF — settings.py: "EXCEPTION_HANDLER": "app.views.custom_exception_handler"
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
resp = exception_handler(exc, context) # стандартный сначала
if resp is not None:
resp.data = {"error": resp.data}
return resp
⚠️ Ловушка: Не возвращайте сырые трейсбеки клиенту в production (утечка внутренней структуры) — это особенно опасно при Django DEBUG=True. Логируйте детали на сервере, отдавайте клиенту обобщённое сообщение и request_id.
31Как настроить CORS в Django/DRF, FastAPI и Flask?
middle
Короткий ответ: CORS (Cross-Origin Resource Sharing) — механизм, разрешающий браузеру обращаться к API с другого origin. Настраивается middleware/расширением со списком разрешённых origins, методов и заголовков. Django — django-cors-headers, FastAPI — CORSMiddleware, Flask — flask-cors.
Подробно:
# FastAPI (Starlette)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST"],
allow_headers=["*"],
allow_credentials=True,
)
# Django — pip install django-cors-headers
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...] # как можно выше
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True
# Flask — pip install flask-cors
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"])
CORS — это проверка на стороне браузера, не сервера. Сервер просто отдаёт заголовки Access-Control-Allow-*, а enforcement делает браузер.
⚠️ Ловушка: allow_origins=["*"] ВМЕСТЕ с allow_credentials=True запрещено спецификацией — браузер отвергнет. Нужно перечислять конкретные origins. И allow_origins=["*"] в production — дыра: любой сайт сможет дёргать ваше API от имени пользователя (при cookie-auth особенно опасно).
32Как реализуют аутентификацию в DRF и FastAPI?
middle
Короткий ответ: DRF использует authentication classes (Session, Token, JWT через сторонние пакеты), заполняющие request.user. FastAPI использует security-зависимости (OAuth2PasswordBearer, HTTPBearer, APIKeyHeader) через Depends, которые валидируют токен и возвращают пользователя.
Подробно:
# DRF — JWT через djangorestframework-simplejwt
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
# request.user заполняется автоматически из токена
# FastAPI — OAuth2 + JWT
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
import jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.PyJWTError:
raise HTTPException(status_code=401)
return get_user(payload["sub"])
@app.get("/me")
async def me(user = Depends(get_current_user)):
return user
Различие в философии: DRF — конфигурация классов (декларативно в settings), FastAPI — зависимости (явно в сигнатуре, видно в OpenAPI).
⚠️ Ловушка: Не путайте authentication (кто ты) и authorization (что тебе можно). Также при JWT помните: токены нельзя «отозвать» без дополнительной инфраструктуры (blacklist/короткий TTL + refresh) — это не сессии. Храните SECRET вне кода и всегда указывайте algorithms=[...] явно (иначе уязвимость alg=none).
33Как тестируют Django и FastAPI приложения?
middle
Короткий ответ: Django — django.test.TestCase (оборачивает каждый тест в транзакцию с откатом) + Client для запросов, либо pytest-django с фикстурами. FastAPI — TestClient (на базе httpx/Starlette) либо async httpx.AsyncClient. Все используют тестовую БД.
Подробно:
# Django + pytest-django
import pytest
@pytest.mark.django_db
def test_article_list(client):
Article.objects.create(title="X")
resp = client.get("/articles/")
assert resp.status_code == 200
# Django TestCase
from django.test import TestCase
class ArticleTests(TestCase):
def setUp(self):
Article.objects.create(title="X") # откатится после теста
def test_list(self):
resp = self.client.get("/articles/")
self.assertEqual(resp.status_code, 200)
# FastAPI
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create():
resp = client.post("/items/", json={"name": "Book", "price": 10})
assert resp.status_code == 200
assert resp.json()["name"] == "Book"
# подмена зависимостей в тестах
app.dependency_overrides[get_db] = override_get_db
⚠️ Ловушка: TestCase оборачивает тест в транзакцию и откатывает — поэтому код, проверяющий поведение коммитов или transaction.on_commit хуков, не сработает; для этого используйте TransactionTestCase. В FastAPI TestClient синхронный (запускает event loop внутри) — для тестирования настоящей конкурентности нужен httpx.AsyncClient с ASGITransport.
34Как обрабатываются статика, медиа и шаблоны?
junior
Короткий ответ: Static — CSS/JS/изображения приложения (STATIC_URL, collectstatic). Media — загруженные пользователями файлы (MEDIA_URL/MEDIA_ROOT). Шаблоны: Django Templates (свой язык) или Jinja2 (Flask/FastAPI). В production статику отдаёт nginx/CDN, не Python.
Подробно:
# Django settings
STATIC_URL = "/static/"
STATIC_ROOT = "/var/www/static/" # сюда collectstatic собирает всё
MEDIA_URL = "/media/"
MEDIA_ROOT = "/var/www/media/" # загрузки пользователей
# python manage.py collectstatic -> собирает статику со всех apps
Шаблонизаторы:
{# Django Template #}
{% for a in articles %}
<li>{{ a.title|upper }}</li> {# фильтры через | #}
{% endfor %}
{% url 'detail' pk=a.pk %}
{# Jinja2 (Flask / FastAPI) — больше похож на Python #}
{% for a in articles %}
<li>{{ a.title | upper }}</li>
{% endfor %}
{{ url_for('detail', pk=a.pk) }}
Django Templates намеренно ограничены (нельзя произвольный Python в шаблоне); Jinja2 мощнее (выражения, вызовы) и быстрее.
⚠️ Ловушка: Не отдавайте статику/медиа через Django в production (DEBUG=True обслуживает их dev-сервером, но это медленно и небезопасно). Используйте nginx/CDN или WhiteNoise для статики. И медиа (пользовательские файлы) нельзя смешивать со static — их нельзя доверять, нужна проверка типа/размера.
35Как деплоят Python веб-приложения и сколько воркеров ставить?
middle
Короткий ответ: Стандарт: nginx (reverse proxy, TLS, статика) → gunicorn/uvicorn (app-сервер с N воркерами) → приложение. Для WSGI/CPU-bound число воркеров ≈ 2 * CPU + 1. Для async (uvicorn) — обычно по числу ядер, конкурентность даёт event loop.
Подробно:
# nginx — фронт: TLS, статика, проксирование
server {
listen 443 ssl;
location /static/ { alias /var/www/static/; } # статику отдаёт nginx
location / {
proxy_pass http://127.0.0.1:8000; # в gunicorn/uvicorn
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
# WSGI (Django/Flask): 4 ядра -> 2*4+1 = 9 воркеров
gunicorn myproject.wsgi:application --workers 9 --bind 127.0.0.1:8000
# ASGI (FastAPI): обычно ~= число ядер
gunicorn main:app -k uvicorn.workers.UvicornWorker --workers 4
Формула 2 * CPU + 1: пока один воркер ждёт I/O, другой может работать на том же ядре. Это эвристика для sync-воркеров. Для async-воркеров много процессов не нужно (один воркер уже конкурентен) — берут ~число ядер для утилизации всех CPU.
Зачем nginx перед app-сервером: TLS-терминация, буферизация медленных клиентов (защита от slowloris), отдача статики, балансировка, сжатие, rate-limit.
⚠️ Ловушка: Слишком много воркеров = переподписка по памяти (каждый воркер держит свою копию приложения, БД-пул, кэш) и деградация. Учитывайте RAM, а не только CPU. И 2*CPU+1 — это про процессы; не путайте с потоками и не применяйте слепо к async-воркерам.
36Как интегрируют фоновые задачи через Celery?
middle
Короткий ответ: Celery — распределённая очередь задач: приложение кладёт задачу в брокер (Redis/RabbitMQ), отдельные воркеры её забирают и выполняют асинхронно. Используется для долгих/тяжёлых операций (отправка писем, генерация отчётов, обработка изображений), чтобы не блокировать веб-запрос.
Подробно:
# celery_app.py
from celery import Celery
celery = Celery("myapp", broker="redis://localhost:6379/0",
backend="redis://localhost:6379/1")
@celery.task(bind=True, max_retries=3)
def send_report(self, user_id):
try:
generate_and_email(user_id)
except SMTPError as exc:
raise self.retry(exc=exc, countdown=60) # ретрай через 60 сек
# в view (Django/FastAPI)
@app.post("/reports/")
def create_report(user_id: int):
send_report.delay(user_id) # ставит в очередь, возвращает сразу
return {"status": "queued"}
# запуск воркера и периодических задач
celery -A myapp worker --loglevel=info --concurrency=4
celery -A myapp beat # планировщик (cron-like задачи)
Компоненты: брокер (транспорт задач), воркеры (исполнители), backend (хранение результатов), beat (расписание).
⚠️ Ловушка: Передавайте в задачу ID, а не сами объекты (модели/сессии) — объект сериализуется и устаревает; правильно загрузить свежий из БД внутри задачи. И в Django частая ошибка — поставить задачу в Celery до коммита транзакции: воркер стартует и не находит запись. Решение — transaction.on_commit(lambda: task.delay(id)).
37Как выбрать между Django, FastAPI и Flask?
concept
Короткий ответ: Django — «батарейки в комплекте» для полноценных приложений с БД, админкой, auth, формами (монолит, CMS, классический backend). FastAPI — современный async API-first сервис (микросервисы, ML-инференс, высокая конкурентность I/O). Flask — микрофреймворк для маленьких/гибких приложений и прототипов.
Подробно:
| Критерий | Django | FastAPI | Flask |
|---|---|---|---|
| Тип | full-stack «батарейки» | API-first, async | микрофреймворк |
| ORM | свой, встроенный | нет (SQLAlchemy/Tortoise) | нет (расширения) |
| Admin | да, мощный | нет | нет |
| Async | частичный (с 3.x) | нативный | ограниченный |
| Валидация | forms/serializers (DRF) | Pydantic (встроено) | вручную/расширения |
| Автодоки | через DRF (надстройки) | OpenAPI из коробки | расширения |
| Кривая входа | высокая | средняя | низкая |
| Лучшее для | монолиты, CMS, CRUD | микросервисы, ML API | прототипы, малые сервисы |
Выбор по сценарию:
- Django (+DRF): проект с админкой, сложной доменной моделью, auth/permissions «из коробки», командой, ценящей конвенции. Классический backend и REST API поверх БД.
- FastAPI: новый сервис с упором на API, высокая конкурентность I/O (внешние API, WebSocket), нужны автодоки и строгая валидация, ML-инференс эндпоинты.
- Flask: небольшой сервис, прототип, нестандартная архитектура, минимум зависимостей, полный контроль над стеком.
⚠️ Ловушка: «FastAPI быстрее Django, значит берём FastAPI» — ошибочная логика. Django даёт огромную экосистему (admin, ORM, auth, формы), на воссоздание которой в FastAPI уйдёт много времени. Скорость фреймворка редко бывает бутылочным горлышком — им чаще оказывается БД. Выбирайте по задаче и экосистеме, а не по бенчмаркам «hello world».
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.