Материал

FastAPI / Django

Теория

SOLID

Принципы SOLID - это набор пяти основных принципов объектно-ориентированного программирования

Принцип единственной ответственности (Single Responsibility Principle, SRP):

Этот принцип гласит, что каждый класс должен быть ответственен только за одну задачу или функцию. Если класс делает слишком много, его сложно поддерживать и изменять. Поэтому важно разделять функциональность на более мелкие, логически связанные части.

Принцип открытости/закрытости (Open/Closed Principle, OCP):

Согласно этому принципу, программные сущности, такие как классы, модули и функции, должны быть открыты для расширения, но закрыты для модификации. Это означает, что изменение поведения сущности должно быть возможно путем добавления нового кода, а не изменения существующего.

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP):

Этот принцип гласит, что объекты базовых классов должны быть заменяемы любыми объектами их производных классов без изменения правильности программы. Другими словами, производные классы должны соответствовать интерфейсу базовых классов.

Принцип разделения интерфейса (Interface Segregation Principle, ISP):

Этот принцип гласит, что клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо этого интерфейсы должны быть разделены на более мелкие и специфические, чтобы клиенты могли использовать только те части интерфейса, которые им действительно нужны.

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP):

Согласно этому принципу, модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Также он гласит, что абстракции не должны зависеть от деталей, но детали должны зависеть от абстракций.

Dependency Injection

Dependency Injection (DI) - это паттерн проектирования, который используется для управления зависимостями между компонентами в программном обеспечении. Он заключается в передаче объектов (зависимостей) вместо их создания внутри класса или функции. Это делает код более гибким, модульным и уменьшает связанность между компонентами.

Основа Dependency Injection:

  1. Инверсия управления (Inversion of Control):
    При использовании Dependency Injection контроль над созданием и управлением зависимостями переходит из класса или функции во внешний источник, например, в контейнер зависимостей. Это позволяет легко заменять или изменять зависимости без изменения кода.
  2. Отделение создания и использования зависимостей:
    Dependency Injection отделяет процесс создания объектов от их использования. Это означает, что объекты, зависящие от других объектов, получают их извне, вместо создания их самостоятельно. Примеры реализации DI:
  • Конструкторная инъекция:
    Зависимости передаются в конструктор объекта.
class MyClass:
    def __init__(self, dependency):
        self.dependency = dependency
  • Методическая инъекция:
    Зависимости передаются через методы объекта.
class MyClass:
    def set_dependency(self, dependency):
        self.dependency = dependency
  • Инъекция через аргументы функций:
    Зависимости передаются в функции в качестве аргументов.
def my_function(dependency):
    # используем зависимость
    pass

Пример в python

class Engine:
    def start(self):
        print("Engine started")

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        self.engine.start()

# Создаем объекты зависимостей
engine = Engine()
car = Car(engine)

# Запускаем автомобиль
car.start()

Пример в fastAPI

from fastapi import Depends, FastAPI

app = FastAPI()

async def get_db_connection():
    # Логика для получения подключения к базе данных
    return "FakeDBConnection"

@app.get("/")
async def read_root(db: str = Depends(get_db_connection)):
    return {"message": f"Database connection: {db}"}

Сравнение Django и FastAPI

  1. Тип приложений:
  • Django: Полнофункциональный фреймворк, ориентированный на создание полномасштабных веб-приложений. Он включает множество встроенных инструментов для работы с базами данных, аутентификации, административного интерфейса и т. д.
  • FastAPI: Фреймворк для создания API на основе ASGI, аннотаций типов и OpenAPI. Он поддерживает как синхронные, так и асинхронные обработчики.
  1. Синхронность и асинхронность:
  • Django: Поддерживает ASGI, асинхронные views, middleware и часть асинхронного ORM API. Некоторые компоненты и сторонние библиотеки остаются синхронными; при их вызове из async-кода требуется корректный адаптер.
  • FastAPI: Позволяет объявлять обработчики через def и async def. Асинхронность полезна прежде всего при большом количестве конкурентных I/O-операций и сама по себе не ускоряет CPU-bound-код.
  1. Скорость разработки:
  • Django: Имеет высокую скорость разработки благодаря множеству встроенных инструментов и конвенциям по разработке.
  • FastAPI: Предоставляет простой и интуитивно понятный интерфейс для создания веб-API. За счет автоматической генерации документации и встроенной валидации данных с использованием Pydantic скорость разработки также может быть высокой.
  1. Производительность:
  • Django и FastAPI могут обслуживать production-нагрузку. Реальная производительность зависит от БД, внешних сервисов, модели конкурентности, кода приложения и инфраструктуры; выбор фреймворка нельзя сводить к синхронности или результатам микробенчмарка.
  1. Общее использование:
  • Django: Широко используется для создания веб-приложений всех видов, включая блоги, электронную коммерцию, CRM и многие другие.
  • FastAPI: Часто используется для создания микросервисов и веб-API, особенно в сферах, где важна скорость и эффективность, таких как машинное обучение и обработка больших объемов данных.

TOP-4 вопроса на Django-собесах

Вопросы из данной категории надо знать на зубок и суметь ответить на них в любое время дня и ночи

select_related, prefetch_related, select_for_update

select_related() и prefetch_related() - это методы оптимизации запросов к базе данных в Django, которые позволяют предварительно загружать связанные объекты, чтобы снизить количество запросов к базе данных. Вот примеры использования select_related() и prefetch_related():

  1. select_related():
from myapp.models import Author, Book

# Выборка всех книг вместе с информацией об авторах
books = Book.objects.select_related('author').all()

for book in books:
    print(book.title, book.author.name)

Этот код выполняет один SQL-запрос, чтобы выбрать все книги вместе с информацией об их авторах. select_related('author') указывает Django предварительно загрузить связанные объекты авторов. 2. prefetch_related():

from myapp.models import Author, Book

# Выборка всех авторов вместе с их книгами
authors = Author.objects.prefetch_related('book_set').all()

for author in authors:
    print(author.name)
    for book in author.book_set.all():
        print(book.title)

Этот код обычно выполняет два SQL-запроса: один для авторов и один для связанных книг, после чего Django объединяет результаты в Python. Метод prefetch_related('book_set') особенно полезен для many-to-many и обратных связей, которые нельзя загрузить обычным SQL JOIN через select_related(). 3. select_for_update(): select_for_update() выполняет SELECT ... FOR UPDATE на поддерживаемых СУБД и блокирует выбранные строки для конкурирующих изменений до конца транзакции. Сам метод не делает произвольный набор операций атомарным: его используют внутри transaction.atomic(), а точная семантика зависит от СУБД и параметров запроса.

from django.db import transaction
from myapp.models import MyModel

# Начинаем транзакцию
with transaction.atomic():
    # Выполняем выборку строк с блокировкой для чтения и обновления
    queryset = MyModel.objects.select_for_update().filter(id=1)

    # Выполняем обновление строк в рамках транзакции
    for obj in queryset:
        obj.field = 'new_value'
        obj.save()

В этом примере:

  1. Мы начинаем транзакцию с помощью transaction.atomic(), чтобы обеспечить атомарность операций.
  2. Мы выполняем выборку строк из модели MyModel с помощью select_for_update(), чтобы заблокировать эти строки для чтения и последующего обновления.
  3. Затем мы обновляем значения полей в выбранных строках и сохраняем изменения в рамках транзакции. Использование select_for_update важно в ситуациях, когда необходимо гарантировать, что выбранные строки не будут изменены другими процессами или потоками до завершения текущей операции. Это позволяет избежать гонки за ресурсами и обеспечивает согласованность данных.

Какая архитектура у Django? Где писать логику view, модель, может еще где-то?

Архитектура Django является MVC-подобной, но имеет некоторые особенности, из-за которых ее иногда называют MTV (Model-Template-View).

Модели (Models):

Модели Django представляют собой классы Python, которые отображаются на таблицы в базе данных. Они определяют структуру данных вашего приложения и обеспечивают взаимодействие с базой данных.

Представления (Views):

Представления Django отвечают за обработку запросов и возвращение ответов. Они получают данные из моделей, обрабатывают их и передают результаты в шаблоны или возвращают их в виде HTTP-ответов.

Шаблоны (Templates):

Шаблоны Django используются для генерации динамических HTML-страниц. Они содержат HTML-код с встроенными директивами и переменными шаблонизации, которые заполняются данными из представлений.

Через какие компоненты идет request в Django?

Иллюстрация к материалу «Методичка FastAPI / Django»

Какие минусы видишь в Django, где бы использовал ее, а где нет?

Тяжеловесность:

Из-за большого количества встроенных функций и компонентов Django может показаться излишне тяжеловесным для небольших проектов или прототипов. Некоторые разработчики предпочитают более легковесные фреймворки для таких задач.

Сложность изучения:

Django имеет довольно крутой кривой обучения, особенно для новичков в веб-разработке. Он предоставляет множество функций и компонентов, которые могут быть сложными для освоения, особенно если вы только начинаете.

Монолитность:

Django предполагает определенную структуру проекта и методологию разработки, что может ограничивать гибкость и расширяемость приложений, особенно если вам нужно интегрировать его с существующими системами или использовать другие подходы к разработке.

Где использовать Django:

  1. Веб-приложения с базой данных: Django отлично подходит для создания веб-приложений, в которых требуется работа с базой данных, таких как блоги, интернет-магазины, CRM-системы и т. д. Его ORM и инструменты администрирования баз данных значительно упрощают работу с данными.
  2. Проекты с быстрым запуском: Если вам нужно быстро развернуть веб-приложение с минимальными затратами времени и усилий, Django предоставляет готовые компоненты и шаблоны, которые позволяют быстро начать разработку.
  3. Крупные проекты с командой разработчиков: Django хорошо подходит для крупных проектов с большим количеством разработчиков. Он обеспечивает хорошую организацию кода и разделение обязанностей, что упрощает совместную работу над проектом.

Где не стоит использовать Django:

  1. Микросервисы и архитектура событийно-ориентированных систем: Для отдельных небольших сервисов может быть удобнее выбрать более минималистичный фреймворк, например FastAPI. В Django тоже можно построить API или событийную систему; выбор зависит от требований, команды и уже используемой инфраструктуры.
  2. Проекты, требующие высокой производительности и масштабируемости: Для проектов, требующих высокой производительности и масштабируемости, особенно если они связаны с большим объемом запросов или операций в реальном времени, могут быть более подходящими альтернативами фреймворки, специализированные на быстрой и эффективной обработке запросов, такие как FastAPI, Node.js или Go.
  3. **Голые API сервисы: **Сервисы, в которых не нужен никакой функционал из “коробки” Django. То есть, никаких админок и прочей шелухи, только ручки и ORM для взаимодействия с БД

База данных и ORM

F, Q objects, Annotate.

  1. F-объект:
    F-объект позволяет вам обращаться к значениям полей модели и использовать их в выражениях для фильтрации, обновления и аннотации без необходимости извлечения их из базы данных. Он представляет собой ссылку на значение поля модели и может использоваться для сравнения, сложения и вычитания с другими полями или константами.
from myapp.models import MyModel
from django.db.models import F

# Увеличение значения поля на 1
MyModel.objects.filter(id=1).update(count=F('count') + 1)

# Сравнение значений двух полей
MyModel.objects.filter(count__gt=F('limit'))
  1. Q-объект:
    Q-объект позволяет вам строить сложные запросы с использованием логических операторов И, ИЛИ и НЕ. Он полезен, когда вам нужно создать запрос с несколькими условиями, которые могут быть комбинированы по разным правилам.
from myapp.models import MyModel
from django.db.models import Q

# Запрос с условием ИЛИ
MyModel.objects.filter(Q(name='John') | Q(name='Alice'))

# Запрос с условием И и ИЛИ
MyModel.objects.filter(Q(name='John') & ~Q(age=30))
  1. Менеджер objects:
    Менеджер objects предоставляет удобные методы для выполнения различных операций с моделями данных, таких как создание, обновление, удаление и выборка. Это базовый менеджер, который используется по умолчанию в моделях Django.
from myapp.models import MyModel

# Создание нового объекта
MyModel.objects.create(name='John', age=30)

# Обновление объекта
obj = MyModel.objects.get(id=1)
obj.name = 'Alice'
obj.save()

# Удаление объекта
MyModel.objects.filter(name='John').delete()

# Выборка объектов
MyModel.objects.all()
  1. annotate:
    Метод annotate позволяет добавлять вычисляемые агрегированные значения к каждому объекту в запросе. Это полезно, когда вам нужно выполнить агрегацию по значениям поля и добавить результаты в каждый объект результата запроса. Тогда как aggregate() возвращает одно значение агрегации для всего набора данных.
from myapp.models import MyModel
from django.db.models import Count

# Подсчет количества объектов для каждого значения поля
queryset = MyModel.objects.annotate(num_items=Count('name'))

# Получение агрегированных значений для каждого объекта
for obj in queryset:
    print(obj.name, obj.num_items)

Как исполняются queryset и цикл жизни?

В Django QuerySet представляет собой объект, который предоставляет интерфейс для выполнения запросов к базе данных и извлечения данных из нее. Цикл жизни QuerySet включает в себя несколько этапов:

  1. Создание QuerySet:
    QuerySet создается при вызове методов менеджера объектов модели, таких как all(), filter(), exclude() и других.
  2. Формирование запроса:
    При создании QuerySet Django строит SQL-запрос, основываясь на вызванных методах и фильтрах, примененных к QuerySet. В этом этапе формируются условия, выбираются поля, определяется порядок сортировки и другие параметры запроса.
  3. Выполнение запроса:
    QuerySet **ЛЕНИВО **используется, например, в цикле или при вызове метода list(), Django отправляет SQL-запрос к базе данных для извлечения данных. Результаты запроса возвращаются в виде объектов моделей или значений полей.
  4. Извлечение данных:
    После выполнения запроса Django получает данные из базы данных и преобразует их в объекты моделей или другие структуры данных, в зависимости от запроса.
  5. Использование данных:
    Полученные данные могут быть использованы в приложении, например, для отображения на веб-странице, обработки в представлении или передачи в шаблон для отображения.
  6. Повторное использование результата:
    После вычисления QuerySet Django обычно сохраняет результаты во внутреннем кэше этого объекта. Специально вызывать close() не нужно, а delete() удаляет строки из базы и не относится к освобождению ресурсов. Важно понимать ленивое вычисление и кэширование QuerySet, чтобы не создавать лишние запросы и не удерживать большие результаты в памяти дольше необходимого.

bulk_create() и bulk_update() — как устроены и что происходит на уровне SQL

  1. bulk_create(): Метод bulk_create() позволяет создавать несколько объектов модели одновременно, что может быть гораздо быстрее, чем создание каждого объекта по отдельности с использованием метода create(). Пример использования bulk_create():
# Django массово вставит объекты одним запросом или несколькими batches — зависит от backend и batch_size.
from myapp.models import MyModel

# Создание списка объектов модели MyModel
objects_to_create = [
    MyModel(name='Object 1'),
    MyModel(name='Object 2'),
    MyModel(name='Object 3'),
]

# Массовое создание объектов
MyModel.objects.bulk_create(objects_to_create)
  1. bulk_update(): Метод bulk_update() позволяет обновлять несколько объектов модели одновременно, что может быть полезно, когда вы хотите обновить много объектов сразу, например, при массовой обработке данных. Пример использования bulk_update():
# Django массово обновит объекты одним запросом или несколькими batches — зависит от backend и batch_size.
from myapp.models import MyModel

# Получение списка объектов для обновления
objects_to_update = MyModel.objects.filter(some_filtering_condition)

# Массовое обновление полей в объектах
for obj in objects_to_update:
    obj.some_field = 'new_value'
MyModel.objects.bulk_update(objects_to_update, ['some_field'])

bulk_create() и bulk_update() могут заметно сократить число запросов, но не вызывают save() модели и не отправляют сигналы pre_save/post_save. У них также есть ограничения для multi-table inheritance и many-to-many связей — перед использованием проверьте документацию вашей версии Django.

Как реализовать having или distinct на ORM?

В Django ORM можно реализовать функционал, аналогичный HAVING в SQL, с использованием методов annotate() и filter(), а также distinct() для функционала DISTINCT.

  1. HAVING: В SQL запросах HAVING используется для фильтрации группированных данных. В Django ORM аналогичный функционал можно получить, используя annotate() и filter(). Например, чтобы отфильтровать группы, у которых количество объектов больше определенного значения:
# Этот запрос вернет группы объектов модели MyModel, у которых количество объектов больше 5.
from django.db.models import Count

# Фильтрация групп, у которых количество объектов больше 5
result = MyModel.objects.values('group').annotate(count=Count('id')).filter(count__gt=5)
  1. DISTINCT: В SQL DISTINCT используется для выбора уникальных значений из столбца. В Django ORM аналогичный функционал можно получить с помощью метода distinct().
# Этот запрос вернет уникальные значения поля field из модели MyModel.
# Выбор уникальных значений поля field
unique_values = MyModel.objects.values_list('field', flat=True).distinct()

Делал ли django migrations, писал ли свои?

Django Migrations - это механизм, который позволяет автоматически создавать и применять изменения в базе данных при изменении структуры моделей Django.

# models.py
from django.db import models

class MyModel(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()

# Выполнение миграции в терминале:

# python manage.py makemigrations >>
# python manage.py migrate

Этот код определяет модель MyModel с двумя полями: name и age. После выполнения команд makemigrations и migrate, эти изменения будут применены к базе данных, и таблица myapp_mymodel будет создана соответствующим образом.

Блокировки, transaction.atomic() - что использовал? Когда возникает?

Блокировки и transaction.atomic() - это инструменты, используемые в Django для управления транзакциями базы данных.

  1. Блокировки (Locks):
    Блокировки в базе данных используются для контроля доступа к ресурсам базы данных с целью предотвращения конфликтов и сохранения целостности данных. В Django вы можете использовать блокировки для защиты критических секций кода от параллельных операций записи.
  2. transaction.atomic():
    transaction.atomic() - это декоратор или контекстный менеджер, который обеспечивает атомарность выполнения блока кода в рамках транзакции базы данных. Нужно для:
  3. согласованность данных в базе данных
  4. несколько операций должны быть выполнены атомарно
  5. если одновременный доступ к ресурсам базы данных может привести к нежелательным последствиям

Архитектура и Design Patterns

Какая архитектура у Django?

Архитектура Django является MVC-подобной, но имеет некоторые особенности, из-за которых ее иногда называют MTV (Model-Template-View). Вот основные компоненты архитектуры Django:

  • Модели (Models):
  • Представления (Views):
  • Шаблоны (Templates):
  • Маршрутизация (URL Dispatcher):
  • ORM (Object-Relational Mapping):
  • Формы (Forms):
  • Административная панель (Admin):

Использовали ли сигналы?

Django Signals - это механизм, который позволяет отправлять сигналы при наступлении определенных событий в вашем приложении Django. Концепции Django Signals:

  1. Создание сигнала:
    Для создания сигнала в Django вы можете использовать django.dispatch.Signal. Обычно это делается в файле signals.py вашего приложения.
from django.dispatch import Signal

# Определение сигнала
my_signal = Signal()
  1. Подписка на сигналы:
    После создания сигнала вы можете подписаться на него с помощью декоратора receiver или метода connect. Обычно это делается в файле apps.py вашего приложения или в файле signals.py.
from django.dispatch import receiver
from myapp.signals import my_signal

# Подписка на сигнал
@receiver(my_signal)
def my_signal_handler(sender, **kwargs):
    # Обработка сигнала
    pass
  1. Отправка сигнала:
    Чтобы отправить сигнал, вы вызываете метод send() для объекта сигнала, передавая отправителя и необязательные аргументы.
from myapp.signals import my_signal

# Отправка сигнала
my_signal.send(sender=self, arg1=value1, arg2=value2)
  1. Обработка сигнала:
    Когда сигнал отправляется, все зарегистрированные обработчики срабатывают. Обработчики получают отправителя сигнала и любые дополнительные аргументы, переданные при отправке сигнала.
  2. Стандартные сигналы Django:
    Django также предоставляет ряд встроенных сигналов, которые могут использоваться для обработки различных событий в вашем приложении, таких как создание объекта, обновление объекта, удаление объекта и другие.

Какое наследование моделей в Django?

В Django есть три основных варианта наследования моделей: абстрактные базовые классы, multi-table inheritance и proxy models.

  1. Абстрактное наследование (Abstract inheritance):
    При использовании абстрактного наследования базовый класс модели не создает отдельной таблицы в базе данных. Вместо этого, его поля просто копируются в таблицу каждой дочерней модели. Дочерние модели могут добавлять дополнительные поля или переопределять существующие.
from django.db import models

class BaseModel(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True

class ChildModel(BaseModel):
    description = models.TextField()

При использовании абстрактного наследования поля name и created_at будут скопированы в таблицу ChildModel. 2. Multi-table inheritance:
Для каждой конкретной модели создаётся отдельная таблица. Django связывает дочернюю строку с родительской автоматически созданной связью OneToOneField с parent_link=True.

from django.db import models

class ParentModel(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

class ChildModel(ParentModel):
    description = models.TextField()

В этом случае Django создаст две таблицы и автоматически добавит в дочернюю модель ссылку на родительскую. Имя столбца по умолчанию связано с родительской моделью, а не обязано быть просто id.

  1. Proxy model:
    Proxy-модель использует ту же таблицу, что и исходная модель, но может менять Python-поведение: методы, менеджер и сортировку по умолчанию. Новая таблица и новые поля при этом не создаются.

Официальные источники