Python: от нуля до разработчика среднего уровня (Django, библиотеки, алгоритмы)

Курс проводит через базовый и продвинутый Python, ключевые практики разработки и экосистему библиотек. Вы освоите алгоритмы и структуры данных, тестирование, работу с базами данных и разработку веб-приложений на Django на уровне уверенного Middle.

1. Старт: синтаксис Python, типы данных и базовые конструкции

Старт: синтаксис Python, типы данных и базовые конструкции

Зачем эта тема нужна для уровня middle

Python-разработчик среднего уровня уверенно читает и пишет код, который:

  • предсказуемо ведёт себя с разными типами данных
  • правильно использует базовые конструкции языка (ветвления, циклы, функции)
  • легко поддерживается командой благодаря стилю и понятным соглашениям
  • В следующих статьях мы будем опираться на эти основы, чтобы перейти к структуре проектов, тестированию, работе с библиотеками, Django и алгоритмам.

    Что такое Python и как выполняется код

    Python — интерпретируемый язык: вы пишете исходный код, а Python запускает его через интерпретатор.

    Практически это означает:

  • вы быстро проверяете гипотезы в интерактивном режиме (REPL)
  • ошибки типов и некоторые другие проблемы часто проявляются во время выполнения
  • Полезные официальные источники:

  • Документация Python 3
  • !Общая схема того, как Python запускает код

    Синтаксис: отступы, блоки, комментарии

    Отступы и блоки кода

    В Python блоки кода определяются отступами, а не фигурными скобками.

    Правила, которые стоит запомнить:

  • стандартный отступ — 4 пробела
  • в одном файле нельзя смешивать разные уровни отступов «как попало»: код станет нечитаемым или сломается
  • Комментарии

  • однострочный комментарий начинается с #
  • Строки документации (docstring)

    Docstring — строка в начале модуля/функции/класса, которая описывает назначение.

    Переменные и присваивание

    Переменная в Python — это имя, которое ссылается на объект.

    Важные особенности:

  • тип хранится у объекта, а не «в переменной»
  • одно и то же имя можно переприсвоить другому объекту (это допустимо, но не всегда хорошо для читаемости)
  • Базовые типы данных

    Числа: int и float

  • int — целые числа
  • float — числа с плавающей точкой
  • Практическое замечание: float может давать «странные» результаты из-за двоичного представления дробей.

    Логический тип: bool

    bool имеет два значения: True и False.

    Операторы:

  • and — логическое «И»
  • or — логическое «ИЛИ»
  • not — логическое «НЕ»
  • Строки: str

    Строки — неизменяемые (это важно) последовательности символов.

    Популярный способ форматирования — f-строки:

    Специальное значение: None

    None означает «отсутствие значения».

    Типичные случаи:

  • функция «ничего не возвращает осмысленного»
  • значение ещё не вычислено
  • Коллекции (контейнеры)

    Список: list

    Список — изменяемая последовательность.

    Кортеж: tuple

    Кортеж — неизменяемая последовательность.

    Кортежи часто используют:

  • для координат и «записей» фиксированной структуры
  • как ключи в словаре (если внутри тоже неизменяемые объекты)
  • Словарь: dict

    Словарь хранит пары ключ -> значение.

    Полезные методы:

    Множество: set

    set — коллекция уникальных элементов.

    Частая задача — убрать дубликаты:

    Изменяемость (mutability): ключ к пониманию ошибок

    Объекты бывают:

  • изменяемые: list, dict, set
  • неизменяемые: int, float, bool, str, tuple, NoneType
  • Почему это важно: когда вы «копируете» переменную, вы копируете ссылку на объект.

    Если нужна поверхностная копия списка:

    Операторы сравнения и истинность (truthiness)

    Сравнение

    Сравнение по идентичности (тот же объект) — is:

    Истинность значений

    В условиях if некоторые значения считаются «ложными»:

  • False
  • None
  • 0, 0.0
  • "" (пустая строка)
  • пустые коллекции: [], {}, set(), ()
  • Это позволяет писать компактно, но важно не терять смысл:

    Ввод/вывод: print() и input()

    print() выводит данные в консоль:

    input() читает строку из консоли:

    Важно: input() всегда возвращает str. Если нужно число — преобразуйте:

    Ветвления: if, elif, else

    Совет по стилю: избегайте слишком сложных условий в одной строке — лучше вынести части в понятные переменные.

    Циклы: for и while

    Цикл for

    for в Python перебирает элементы итерируемого объекта (проще: того, что можно последовательно перечислять).

    range() часто используют для счётчика:

    Если нужны и индекс, и значение — используйте enumerate():

    Цикл while

    while выполняется, пока условие истинно.

    break и continue

  • break завершает цикл
  • continue пропускает текущую итерацию
  • Функции: def, параметры, return

    Функция — именованный кусок кода, который можно вызывать много раз.

    Параметры по умолчанию

    Важное правило: не используйте изменяемые объекты как значения по умолчанию.

    Плохо:

    Хорошо:

    Область видимости имён

    Если имя создано внутри функции, снаружи оно не видно:

    Работа с ошибками: базовое понимание исключений

    Если во время выполнения происходит ошибка, Python выбрасывает исключение.

    Пример:

    Минимальная конструкция обработки:

    Важно: не «глушите» исключения без причины. Обрабатывайте только те, которые реально ожидаете.

    Стиль кода: читаемость как часть профессионализма

    Основной документ по стилю Python — PEP 8:

  • PEP 8 — Style Guide for Python Code
  • Минимальные правила на старте:

  • имена переменных и функций: snake_case
  • классы: CamelCase
  • константы: UPPER_CASE
  • не пишите слишком длинные функции: лучше несколько маленьких
  • Итоги

    Вы изучили фундамент, на котором строится всё остальное в курсе:

  • как устроен синтаксис Python (отступы, блоки, комментарии, docstring)
  • основные типы данных и коллекции
  • изменяемость и почему она влияет на поведение программы
  • базовые управляющие конструкции: if, циклы
  • функции и безопасные значения по умолчанию
  • базовое понимание исключений и важность стиля
  • 2. Функции, модули, пакеты и работа с окружением (venv, pip, poetry)

    Функции, модули, пакеты и работа с окружением (venv, pip, poetry)

    Зачем эта тема нужна для уровня middle

    После базового синтаксиса и простых функций следующий шаг к уровню middle — уметь:

  • проектировать функции так, чтобы ими было удобно пользоваться и сложно ошибиться
  • структурировать код по модулям и пакетам, а не держать всё в одном файле
  • управлять зависимостями проекта и воспроизводимым окружением (чтобы код одинаково запускался у вас, у коллег и на сервере)
  • На практике большинство проблем в реальных проектах начинаются не с синтаксиса, а с:

  • хаоса в импортах
  • непредсказуемых зависимостей
  • разных версий библиотек на разных машинах
  • Эта статья соединяет “код” (функции, модули, пакеты) и “инфраструктуру разработки” (venv, pip, poetry), потому что для Python-разработчика это одна связка.

    Функции как интерфейс: как писать удобно и безопасно

    В прошлой теме вы уже писали функции через def. Теперь усилим понимание: функция — это контракт между автором и пользователем (включая вас через месяц).

    Возврат значения и явность

  • Если функция вычисляет результат, она должна возвращать его через return.
  • Если функция делает действие (например, печатает или пишет в файл), часто она возвращает None (явно или по умолчанию).
  • Плохая практика — “прятать” результат в print():

    Позиционные и именованные аргументы

    Python позволяет вызывать функцию:

  • позиционно: f(10, 20)
  • по имени: f(x=10, y=20)
  • Именованный вызов повышает читаемость в сложных местах.

    Значения по умолчанию и ловушка изменяемых объектов

    Правило из предыдущей статьи критически важно в проектах: изменяемые значения по умолчанию нельзя использовать.

    Почему так? Значение по умолчанию вычисляется один раз при определении функции, а не при каждом вызове.

    args и *kwargs: гибкие функции

  • *args собирает лишние позиционные аргументы в кортеж
  • **kwargs собирает лишние именованные аргументы в словарь
  • Это полезно для обёрток, логирования, адаптеров, интеграций.

    Важно: не делайте каждую функцию “резиновой”. Если функция всегда принимает **kwargs, пользователю легко передать несуществующий параметр и долго искать, почему он не работает.

    Докстринги и минимальная документация

    Докстринг — это “паспорт” функции. Его читают:

  • коллеги
  • вы в IDE
  • генераторы документации
  • Официальный стиль документации строк — PEP 257.

    Модули: как разносить код по файлам и импортировать

    Что такое модуль

    Модуль в Python — это обычный файл *.py.

    Пример структуры:

    Использование в другом файле:

    Варианты импорта

    Рекомендации для читаемости:

  • import module хорош, когда модуль нужен целиком: math.sqrt()
  • from module import name хорош, когда используете 1–2 функции и имя очевидно
  • алиасы (as) уместны для длинных имён или общепринятых сокращений
  • Не используйте from module import *, потому что:

  • вы теряете контроль над тем, какие имена попали в текущий файл
  • сложнее понимать код и отлаживать
  • Как Python находит модуль

    Импорт — это поиск по путям. В упрощённом виде Python смотрит:

  • текущую директорию проекта
  • стандартную библиотеку
  • установленные зависимости из окружения (site-packages)
  • Технические детали: The import system.

    !Диаграмма того, как Python ищет и загружает модули при import

    __name__ == "__main__": код, который можно запускать и импортировать

    Часто файл хотят использовать и как модуль, и как скрипт.

  • при python tool.py условие истинно и выполнится main()
  • при import tool условие ложно, и “побочные эффекты” не выполнятся
  • Это важно для тестируемости и переиспользования.

    Пакеты: как собирать модули в “библиотеку проекта”

    Что такое пакет

    Пакет — это папка с модулями, предназначенная для импорта.

    Современный Python допускает разные варианты, но самый понятный учебный вариант — папка с файлом __init__.py.

    Пример:

    Импорт:

    Зачем нужен __init__.py

    Он делает папку пакетом и может:

  • быть пустым
  • определять публичное API пакета
  • Пример аккуратного API:

    Тогда пользователь может писать:

    Относительные импорты внутри пакета

    Внутри пакета удобно использовать относительные импорты:

    Правило: относительные импорты работают в контексте пакета. Если пытаться запускать модуль пакета напрямую как файл, можно получить проблемы с импортами. В таких случаях запускают через python -m.

    Пример:

    Окружение: зачем нужно изолировать зависимости

    Проблема “работает у меня”

    Если ставить библиотеки глобально в систему, быстро возникает хаос:

  • проект A требует Django 4.x
  • проект B требует Django 5.x
  • вы обновили одну библиотеку и сломали другой проект
  • Решение: виртуальные окружения — отдельные “папки” со своим Python и своими установленными пакетами.

    Официальная документация: venv — Creation of virtual environments.

    !Иллюстрация, почему виртуальные окружения предотвращают конфликты зависимостей

    venv: создание и использование виртуального окружения

    Создание

    В корне проекта:

    Часто имя .venv используют по соглашению.

    Активация

    Windows (PowerShell):

    macOS/Linux (bash/zsh):

    После активации команды python и pip будут указывать на окружение.

    Проверка

    Вы должны увидеть путь внутри .venv.

    Что обычно добавляют в .gitignore

  • .venv/
  • __pycache__/
  • Идея: окружение не коммитят, его воспроизводят по файлам зависимостей.

    pip: установка зависимостей и фиксация версий

    pip — стандартный установщик пакетов Python. Документация: pip documentation.

    Установка пакета

    Установка конкретной версии

    Список установленных пакетов

    Фиксация зависимостей

    Классический подход — requirements.txt:

    И установка у другого разработчика или на сервере:

    Практическая рекомендация для командной разработки:

  • держите версии зависимостей зафиксированными
  • обновляйте зависимости осознанно и проверяйте тестами
  • Общий гайд по упаковке и зависимостям: Python Packaging User Guide.

    Poetry: современное управление зависимостями и сборкой

    Poetry решает сразу несколько задач:

  • создаёт/управляет виртуальным окружением
  • хранит зависимости в pyproject.toml
  • фиксирует точные версии в poetry.lock
  • Документация: Poetry documentation.

    Инициализация проекта

    В папке проекта:

    Или автоматически создать структуру:

    Добавление зависимостей

    Dev-зависимости (например, для тестов):

    Установка зависимостей

  • pyproject.toml хранит “намерение” (какие зависимости нужны)
  • poetry.lock хранит “факт” (какие версии реально были выбраны)
  • Это делает окружение воспроизводимым.

    Запуск команд внутри окружения Poetry

    Или перейти в оболочку окружения:

    Что коммитят в репозиторий при Poetry

    Обычно коммитят:

  • pyproject.toml
  • poetry.lock
  • Не коммитят:

  • само окружение
  • Как выбрать: venv + pip или Poetry

    | Критерий | venv + pip | Poetry | |---|---|---| | Простота старта | Очень простые базовые команды | Чуть больше концепций | | Зависимости | Часто через requirements.txt | pyproject.toml + poetry.lock | | Воспроизводимость | Хорошая при дисциплине версий | Обычно “из коробки” | | Типичный командный Python-проект | Да | Да |

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

    Частые ошибки и как их избегать

  • Устанавливать пакеты “не в то” окружение
  • - проверяйте which python (macOS/Linux) или where python (Windows) - проверяйте sys.executable
  • Смешивать способы управления зависимостями
  • - не стоит одновременно вести и requirements.txt через pip freeze, и poetry.lock как основной источник истины
  • Делать тяжёлые импорты на верхнем уровне модуля без необходимости
  • - это замедляет запуск и может ломать CLI-инструменты
  • Создавать циклические импорты
  • - признак плохой архитектуры: модули слишком сильно зависят друг от друга

    Итоги

    Теперь у вас есть база, чтобы писать и поддерживать реальный проектный код:

  • вы понимаете функции как интерфейс: аргументы, значения по умолчанию, args и *kwargs, докстринги
  • вы умеете разносить код по модулям и контролировать импорты
  • вы понимаете, что такое пакет и как организовать структуру проекта
  • вы умеете работать с окружением через venv, ставить зависимости через pip, фиксировать версии
  • вы знаете, как Poetry объединяет управление зависимостями и воспроизводимость окружения
  • Следующие темы курса будут опираться на это: мы будем подключать библиотеки, собирать более крупные проекты, а позже — работать с Django и тестированием в “правильном” окружении.

    3. ООП и продвинутый Python: исключения, итераторы, генераторы, декораторы, контекстные менеджеры

    ООП и продвинутый Python: исключения, итераторы, генераторы, декораторы, контекстные менеджеры

    Зачем эта тема нужна для уровня middle

    В предыдущих темах вы научились писать функции, разбивать код на модули и управлять окружением. Следующий шаг к уверенной разработке на уровне middle — использовать языковые протоколы Python и понимать, как устроены «типичные» конструкции, на которых держатся библиотеки и фреймворки.

    На практике уровень middle часто проявляется так:

  • вы проектируете код через классы и композицию, когда это упрощает поддержку
  • вы правильно обрабатываете ошибки и не превращаете исключения в «тихий шум»
  • вы понимаете, что такое итератор и генератор, и пишете эффективный код без лишней памяти
  • вы умеете читать и писать декораторы (а в Django они встречаются постоянно)
  • вы используете контекстные менеджеры для управления ресурсами (файлы, соединения, блокировки)
  • Эти темы напрямую пригодятся дальше в курсе при изучении библиотек, Django (включая middleware, декораторы, class-based views) и при написании более надёжного кода.

    Объектно-ориентированное программирование в Python

    Объекты и классы

    В Python всё является объектами: числа, строки, функции, классы.

  • объект содержит данные (состояние) и поведение
  • класс — это «шаблон», по которому создают объекты
  • Простейший класс:

    Ключевые элементы:

  • __init__ — конструктор: вызывается при создании объекта
  • self — ссылка на текущий объект
  • self.username — атрибут объекта
  • Атрибуты экземпляра и атрибуты класса

    Атрибуты бывают:

  • атрибуты экземпляра (у каждого объекта свои)
  • атрибуты класса (общие на класс)
  • Важно не перепутать: если записать self.total_created = ..., вы создадите атрибут экземпляра, который «скроет» атрибут класса только для этого объекта.

    Методы: обычные, classmethod и staticmethod

    Python поддерживает три распространённых типа методов.

    #### Обычный метод

    Получает self и работает с объектом.

    #### @classmethod

    Получает cls (сам класс). Удобно для альтернативных конструкторов.

    #### @staticmethod

    Не получает ни self, ни cls. Это просто функция, «положенная» внутрь класса для смысловой группировки.

    Практическое правило:

  • если метод использует состояние объекта — обычный метод
  • если метод создаёт объект или работает с классом — @classmethod
  • если метод не зависит ни от объекта, ни от класса — @staticmethod (или вынести в модуль)
  • Инкапсуляция и соглашение про «приватность»

    В Python нет жёстких модификаторов доступа как в некоторых языках. Вместо этого используется соглашение:

  • _name — «внутренний» атрибут: не трогать снаружи без необходимости
  • __namename mangling (переименование внутри класса), применяется реже
  • Это не защита, а договорённость, которая улучшает поддерживаемость.

    Наследование и композиция

    Наследование — отношение «является».

    Композиция — отношение «имеет».

    Практическое правило для реальных проектов:

  • композиция часто делает архитектуру гибче
  • наследование полезно, когда есть действительно общий контракт и полиморфизм
  • В Django вы встретите оба подхода, но особенно много наследования (например, class-based views), поэтому важно уметь его читать.

    Специальные методы (dunder) и «протоколы» Python

    Многие возможности Python основаны на специальных методах вида __something__. Это позволяет объектам «встраиваться» в язык.

    Примеры:

  • __repr__ — отладочное строковое представление
  • __str__ — «пользовательская» строка
  • __len__ — длина
  • __iter__ и __next__ — итерация
  • __enter__ и __exit__ — контекстный менеджер
  • Официальный список специальных методов: Data model.

    dataclasses: меньше шаблонного кода

    Если класс в основном хранит данные, часто используют dataclasses (стандартная библиотека).

    Это уменьшает «шум» и делает код читабельнее. Документация: dataclasses.

    Исключения: как правильно обрабатывать ошибки

    Что такое исключение

    Исключение — механизм, который прерывает обычный поток выполнения при ошибке или необычной ситуации.

    Основные идеи:

  • исключения удобно поднимать там, где обнаружена проблема
  • исключения удобно обрабатывать там, где есть контекст, что делать дальше
  • Документация: Errors and Exceptions.

    Блоки try/except/else/finally

    Смысл блоков:

  • except — обработка ожидаемых ошибок
  • else — выполняется, только если исключения не было (удобно, чтобы не раздувать try)
  • finally — выполняется всегда (закрытие ресурсов, откат состояний)
  • Практический совет: держите try минимальным, чтобы не «поймать» ошибку из неожиданного места.

    raise и собственные исключения

    Иногда лучше явно определить своё исключение, чтобы отделить ошибки домена от технических.

    Почему это полезно:

  • легче различать ошибки и правильно их обрабатывать
  • понятнее логировать
  • проще строить API функций и модулей
  • Цепочка исключений и raise ... from ...

    Если вы ловите одну ошибку и поднимаете другую, полезно сохранять «причину».

    Так в traceback будет видно исходную причину.

    EAFP против LBYL

    В Python часто используют стиль EAFPeasier to ask forgiveness than permission.

  • EAFP: сначала делаем, при ошибке обрабатываем
  • LBYL: заранее проверяем условия
  • Пример EAFP:

    Это естественно сочетается с исключениями и часто делает код короче.

    Итераторы и итерируемые объекты

    Определения: iterable и iterator

    Важно различать два понятия.

  • iterable (итерируемый объект) — объект, по которому можно пройти циклом for
  • iterator (итератор) — объект, который хранит состояние перебора и умеет возвращать следующий элемент
  • Протоколы:

  • iterable имеет __iter__, который возвращает итератор
  • iterator имеет __next__ и тоже имеет __iter__ (возвращает сам себя)
  • Документация: Iterator Types.

    !Как цикл for получает итератор и элементы через протокол __iter__/__next__

    Как for работает под капотом

    Упрощённо цикл:

    Работает примерно так:

  • it = iter(items)
  • повторять next(it)
  • завершить цикл при StopIteration
  • Напишем свой итератор

    Пример: итератор по числам от start до end включительно.

    Практическое замечание:

  • такой объект является итератором, но он «одноразовый»: второй проход продолжит с текущего состояния
  • если нужен объект, который можно перебирать многократно, обычно делают iterable, который создаёт новый итератор при каждом __iter__
  • Генераторы: ленивые последовательности без лишней памяти

    Генераторная функция и yield

    Генератор — удобный способ писать итераторы. Вместо класса с __next__ вы пишете функцию с yield.

    Плюсы:

  • меньше кода
  • состояние сохраняется автоматически между yield
  • удобно строить пайплайны обработки данных
  • Документация: Generators.

    Почему генераторы важны

    Генератор создаёт элементы по мере запроса. Это особенно полезно, когда:

  • данных много
  • чтение идёт потоком (файл, сеть)
  • вы хотите ранний выход (остановиться, как только нашли нужное)
  • Пример: обработка большого файла построчно.

    yield from

    Если генератор делегирует работу другому генератору или итерируемому объекту, используйте yield from.

    Генераторные выражения

    Это аналог list comprehension, но без создания списка.

    Сравнение:

  • [x * x for x in range(10)] создаёт список целиком
  • (x * x for x in range(10)) создаёт генератор и вычисляет по мере надобности
  • Декораторы: функции, которые расширяют функции

    Идея декоратора

    Декоратор — это функция, которая принимает другую функцию и возвращает новую функцию (обычно обёртку), добавляя поведение.

    В Python функции — объекты, их можно передавать как аргументы и возвращать.

    Простой декоратор

    Строка @debug эквивалентна:

    !Как декоратор «оборачивает» функцию

    Почему нужен functools.wraps

    Без wraps у обёртки теряются имя, докстринг и другие метаданные исходной функции. Это важно для отладки, документации и некоторых фреймворков.

    Документация: functools.wraps.

    Декораторы с параметрами

    Иногда декоратору нужны настройки. Тогда делают функцию, которая возвращает декоратор.

    Где встречаются декораторы в реальных проектах

    Типичные кейсы:

  • логирование и метрики
  • кэширование (functools.lru_cache)
  • контроль доступа и авторизация (в Django очень часто)
  • валидация входных параметров
  • ретраи при временных ошибках сетевых запросов
  • Контекстные менеджеры: управление ресурсами через with

    Зачем нужен with

    Ресурсы нужно освобождать даже при ошибках:

  • файлы нужно закрывать
  • блокировки нужно отпускать
  • соединения нужно корректно завершать
  • with гарантирует корректное завершение «секции».

    Протокол контекстного менеджера: __enter__ и __exit__

    Контекстный менеджер — объект с методами __enter__ и __exit__.

    Ключевой момент:

  • __exit__ вызывается и при нормальном завершении, и при исключении
  • аргументы exc_type, exc, tb содержат информацию об исключении (или None, если его не было)
  • contextlib: удобные инструменты

    Стандартная библиотека даёт готовые помощники. Документация: contextlib.

    #### Декоратор @contextmanager

    Если не хочется писать класс, можно сделать контекстный менеджер генератором.

    #### contextlib.suppress

    Аккуратно подавляет конкретные исключения.

    Используйте подавление точечно: подавлять «всё подряд» опасно, потому что ошибки станут незаметными.

    Как эти темы связаны между собой

    Эти инструменты хорошо работают в связке:

  • контекстные менеджеры часто используют исключения (через __exit__) для корректного завершения
  • генераторы и итераторы — реализация одного протокола, и многие библиотеки строят API вокруг этого
  • декораторы часто используют args и *kwargs, которые вы изучали ранее, чтобы быть универсальными
  • ООП в Python во многом про реализацию протоколов через dunder-методы, а не про «класс ради класса»
  • Итоги

    Теперь у вас есть важный слой «продвинутого Python», который постоянно встречается в реальных проектах:

  • вы понимаете ООП в Python: классы, атрибуты, методы, наследование и композицию
  • вы знаете, как и где правильно использовать исключения, включая else/finally и raise ... from ...
  • вы отличаете iterable и iterator и понимаете, как for работает через протокол итерации
  • вы умеете писать генераторы через yield, применять yield from и генераторные выражения
  • вы понимаете декораторы, умеете сохранять метаданные через functools.wraps и делать декораторы с параметрами
  • вы умеете управлять ресурсами через with, писать свои контекстные менеджеры и использовать contextlib
  • Дальше эти навыки станут опорой при работе с библиотеками, Django и при построении поддерживаемой архитектуры проекта.

    4. Алгоритмы и структуры данных: сложность, сортировки, поиск, рекурсия, графы, основы динамики

    Алгоритмы и структуры данных: сложность, сортировки, поиск, рекурсия, графы, основы динамики

    Зачем эта тема нужна для уровня middle

    Middle Python-разработчик регулярно сталкивается с задачами, где важно не просто “чтобы работало”, а чтобы работало быстро, предсказуемо и масштабируемо. Алгоритмы и структуры данных дают:

  • язык для обсуждения производительности (например, почему “стало медленно”)
  • инструменты выбора правильного решения (какая структура данных подходит)
  • основу для понимания библиотек и фреймворков (Django, ORM, кэши, очереди, индексы)
  • Связь с предыдущими темами курса:

  • вы будете оформлять алгоритмы как функции и модули (тема про функции, модули, пакеты)
  • вы будете писать чистый код с итераторами, генераторами и декораторами (продвинутый Python)
  • вы будете осознанно обрабатывать ошибки входных данных (исключения)
  • Официальные источники для справки:

  • Документация Python: Модули стандартной библиотеки
  • Документация Python: Анализ сложности сортировок list.sort и sorted
  • Время и память: что такое сложность

    Зачем оценивать сложность

    Одна и та же задача может решаться разными способами:

  • решение A работает за секунды на больших данных
  • решение B работает за минуты или часы
  • Чтобы сравнивать решения, обычно говорят о том, как растут затраты при росте размера входных данных .

    Нотация

    читают как “порядок”. Это приближённая оценка роста времени или памяти.

  • — размер входных данных (например, длина списка)
  • — константное время: почти не зависит от
  • — линейное время: растёт пропорционально
  • — квадратичное время: часто возникает при “двух вложенных циклах по данным”
  • — логарифмическое: характерно для бинарного поиска и деревьев
  • — типично для эффективных сортировок
  • Важно понимать смысл без “магии формул”:

  • обычно означает “пару операций”
  • означает “делим задачу примерно пополам много раз”
  • часто означает “сделали шагов для каждого элемента”
  • !Визуальное сравнение того, как растёт время разных классов алгоритмов

    Частая ошибка: считать только количество строк кода

    Два фрагмента могут выглядеть похоже, но отличаться по сложности.

    Пример: проверка, есть ли дубликаты.

    #### Вариант с вложенными проверками

    Сложность по времени: обычно .

    #### Вариант через set

    В среднем: по времени и по памяти.

    Базовые структуры данных и их свойства

    Что даёт правильный выбор структуры

    Структура данных — это не просто “контейнер”, а набор гарантий:

  • как быстро добавлять элементы
  • как быстро искать
  • как быстро удалять
  • сколько памяти это будет стоить
  • Список list

    list в Python — динамический массив.

    Типичные свойства:

  • доступ по индексу a[i]: обычно
  • append: амортизированно
  • вставка/удаление в середине: (нужно сдвигать элементы)
  • Словарь dict и множество set

    dict и set основаны на хеш-таблицах.

    Типичные свойства (в среднем):

  • проверка наличия x in s:
  • доступ d[key]:
  • Важно:

  • худшие случаи существуют, но в большинстве прикладных задач средняя оценка достаточно надёжна
  • ключи в dict и элементы в set должны быть хешируемыми (обычно неизменяемыми)
  • Стек и очередь: что использовать в Python

    Стек — LIFO (последним пришёл, первым вышел). Очередь — FIFO.

  • стек удобно делать обычным list через append и pop
  • очередь лучше делать через collections.deque, потому что list.pop(0) стоит
  • Справка: Документация collections.deque

    Куча (heap) и приоритетная очередь

    Куча нужна, когда вы часто берёте “минимальный” или “максимальный” элемент.

    В Python есть модуль heapq (это мин-куча):

  • вставка:
  • извлечение минимума:
  • Справка: Документация heapq

    Сортировки: идеи и практические выборы

    Зачем сортировать

    Сортировка часто служит “подготовкой” данных:

  • после сортировки можно делать бинарный поиск
  • проще объединять/сравнивать наборы
  • можно решать задачи “двух указателей”
  • Встроенная сортировка Python

    В Python обычно используют:

  • sorted(iterable) возвращает новый список
  • list.sort() сортирует список на месте
  • Обе используют Timsort.

    Ключевые свойства Timsort:

  • средняя и худшая сложность по времени:
  • может работать быстрее на частично отсортированных данных
  • стабильная сортировка (равные элементы сохраняют относительный порядок)
  • Стабильность здесь означает: Bob и Ada имеют одинаковый возраст, и их порядок среди “возраст=20” остаётся как в исходном списке.

    Справка: Документация Python: Sorting HOW TO

    Классические сортировки (чтобы понимать компромиссы)

    #### Сортировка пузырьком

    Идея: многократно “проталкивать” большие элементы вправо.

  • время:
  • память:
  • Используется в обучении, в продакшене почти не нужна.

    #### Сортировка вставками

    Идея: строить отсортированную часть, вставляя элементы в правильную позицию.

  • время: , но хороша на маленьких массивах или почти отсортированных
  • #### Быстрая сортировка (quicksort)

    Идея: выбрать опорный элемент, разделить элементы на меньшие и большие, рекурсивно сортировать части.

  • среднее:
  • худшее: (при плохом выборе опорного)
  • Важно: Python не использует quicksort для sorted, потому что Timsort даёт надёжные гарантии и отлично работает на “реальных” данных.

    Практическое правило выбора

  • если нужно просто отсортировать: используйте sorted или list.sort
  • если нужно часто поддерживать “минимум” по мере поступления данных: используйте heapq
  • если нужен поиск по отсортированному массиву: рассмотрите bisect
  • Поиск: линейный и бинарный

    Линейный поиск

    Идея: пройти элементы подряд.

  • время:
  • Бинарный поиск

    Работает только по отсортированным данным.

    Идея: каждый шаг делит диапазон пополам.

  • время:
  • #### Реализация вручную

    #### Встроенные инструменты: bisect

    Если вам нужно не “найти точное совпадение”, а “найти место для вставки”, используйте модуль bisect.

    Справка: Документация bisect

    Рекурсия: сила и ограничения

    Что такое рекурсия

    Рекурсия — это когда функция вызывает саму себя для решения подзадачи.

    Типичный шаблон:

  • база (условие остановки)
  • переход (сведение задачи к меньшей)
  • Пример факториала:

    Почему рекурсия может быть проблемой в Python

    У рекурсии есть стоимость:

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

    Рекурсия и обход деревьев

    Рекурсия особенно естественна для структур “дерево”.

    Пример: суммирование всех значений в узлах.

    Связь с предыдущей статьёй про ООП и протоколы: дерево удобно моделируется классами, а обход можно делать итератором или генератором.

    Графы: представление и обход

    Что такое граф

    Граф состоит из:

  • вершин (узлов)
  • рёбер (связей)
  • Графы встречаются в задачах:

  • маршрутизация
  • зависимости между задачами
  • социальные сети
  • рекомендации
  • Как хранить граф

    Частый способ — список смежности: dict[vertex] -> list of neighbors.

    Плюсы:

  • экономит память на разреженных графах
  • удобно перебирать соседей
  • BFS: поиск в ширину

    BFS обходит граф “слоями” и на невзвешенных графах находит кратчайшее расстояние по числу рёбер.

    Ключевой инструмент: очередь deque.

    Сложность: обычно , где:

  • — количество вершин
  • — количество рёбер
  • Каждую вершину и ребро мы обрабатываем ограниченное число раз.

    DFS: поиск в глубину

    DFS идёт “вглубь”, пока может.

    Можно реализовать рекурсией, но в Python часто делают итеративно через стек.

    !Наглядная разница между обходом в ширину и в глубину

    Кратчайшие пути со взвешенными рёбрами: Дейкстра

    Если у рёбер есть вес (стоимость), BFS уже не подходит.

    Алгоритм Дейкстры:

  • подходит для неотрицательных весов
  • обычно реализуется через приоритетную очередь (heapq)
  • Сложность зависит от реализации, но практическая мысль такая:

  • heapq добавляет логарифмический фактор из-за операций с кучей
  • Динамическое программирование: основы

    Идея динамики

    Динамическое программирование (ДП) полезно, когда:

  • задача раскладывается на повторяющиеся подзадачи
  • можно сохранить результаты подзадач и не считать заново
  • ДП обычно строится на двух концепциях:

  • перекрывающиеся подзадачи (одно и то же считается много раз)
  • оптимальная подструктура (оптимальное решение строится из оптимальных решений подзадач)
  • Мемоизация: “кэш для рекурсии”

    Пример: числа Фибоначчи. Наивная рекурсия пересчитывает одно и то же.

    lru_cache автоматически запоминает результаты.

    Справка: Документация functools.lru_cache

    Практическое замечание:

  • мемоизация часто превращает “очень медленно” в “быстро”, но увеличивает память
  • Табуляция: снизу вверх

    Вместо рекурсии строим таблицу значений от маленьких к большим.

    Табуляция часто:

  • быстрее (нет накладных расходов рекурсии)
  • проще по памяти (можно хранить только нужные последние значения)
  • !Иллюстрация принципа «снизу вверх» в динамическом программировании

    Как распознавать задачи на ДП

    Частые признаки:

  • “количество способов” (сколько путей, сколько вариантов)
  • “оптимум” (минимум/максимум стоимости)
  • ограничения по шагам (например, можно идти на 1 или 2)
  • повторяющиеся состояния (например, “позиция i и остаток j”)
  • Практика для Python-разработчика: что реально использовать

    Что чаще всего пригодится в проектах

  • set и dict для быстрых проверок и индексации
  • deque для очередей и BFS
  • heapq для задач “всегда брать минимальное” и для Дейкстры
  • sorted с key=... для бизнес-сортировок
  • bisect для вставки/поиска в отсортированных списках
  • Чеклист выбора подхода

  • если нужен “поиск по ключу”: попробуйте dict
  • если нужен “уникальный набор”: set
  • если нужен “порядок поступления и быстро вынимать слева”: deque
  • если нужен “минимум/максимум среди текущих”: heapq
  • если данные отсортированы и нужно быстро искать: бинарный поиск или bisect
  • Итоги

    Вы разобрали фундаментальный набор идей, без которых сложно расти до middle:

  • что такое сложность и как думать про рост времени и памяти
  • свойства ключевых структур данных в Python и их применение
  • сортировки: что даёт sorted, что такое стабильность и почему важно key
  • поиск: линейный и бинарный, а также практическое использование bisect
  • рекурсия: где она уместна и почему в Python важно помнить про глубину стека
  • графы: представление, BFS и DFS, и базовый Дейкстра через heapq
  • динамическое программирование: мемоизация и табуляция как два основных стиля
  • Дальше эти навыки помогут вам увереннее работать с реальными задачами: оптимизировать код, понимать ограничения и выбирать корректные структуры данных, а также легче проходить технические интервью и читать сложный код в библиотеках и Django-проектах.

    5. Инструменты разработчика: Git, отладка, логирование, тестирование (pytest), качество кода

    Инструменты разработчика: Git, отладка, логирование, тестирование (pytest), качество кода

    Зачем эта тема нужна для уровня middle

    До этого в курсе вы изучили синтаксис, модули и окружение, ООП и исключения, а также алгоритмическое мышление. Но в реальной разработке уровень middle определяется не только умением писать код, а умением поддерживать его в команде:

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

    !Базовый рабочий процесс Git от локальных изменений до удалённого репозитория

    Git: контроль версий как основа командной разработки

    Что такое Git и какие проблемы он решает

    Git хранит историю изменений проекта и позволяет:

  • возвращаться к любому состоянию кода
  • параллельно вести разработку в разных ветках
  • безопасно интегрировать изменения через ревью
  • Официальный ресурс: Pro Git.

    Термины, которые нужно понимать

  • репозиторий это папка проекта с историей (.git)
  • коммит это снимок изменений с сообщением
  • ветка это указатель на цепочку коммитов
  • удалённый репозиторий это репозиторий на сервере (GitHub, GitLab)
  • staging area это “индекс” для подготовки коммита
  • Минимальный набор команд на каждый день

    Инициализация и клонирование:

    Проверка статуса и истории:

    Подготовка и фиксация изменений:

    Работа с удалённым репозиторием:

    Ветки и типичный командный workflow

    Базовая модель:

  • main или master это стабильная ветка
  • новая задача делается в feature/<name>
  • изменения попадают в main через pull request и код-ревью
  • Создание и переключение ветки:

    Когда задача готова:

    Merge и rebase: что выбрать и почему

  • merge сохраняет “историческую правду”, но может добавить merge-коммиты
  • rebase делает историю линейной, но переписывает коммиты
  • Практическое правило:

  • для публичных веток, которые уже видят другие разработчики, обычно безопаснее merge
  • rebase удобен, чтобы “привести в порядок” локальную ветку перед отправкой на ревью
  • Обновить свою ветку относительно main через rebase:

    Если после rebase ветка уже была отправлена на сервер, пуш потребуется форсированный:

    --force-with-lease безопаснее, чем --force, потому что не перезатрёт чужие изменения, если ветка на сервере успела измениться.

    .gitignore: что не нужно коммитить

    Обычно в Python-проектах не коммитят:

  • .venv/
  • __pycache__/
  • .pytest_cache/
  • .mypy_cache/
  • .ruff_cache/
  • .idea/ и другие файлы IDE (по договорённости команды)
  • секреты (токены, пароли, .env)
  • Репозиторий шаблонов: gitignore для Python.

    Сообщения коммитов: минимальная дисциплина

    Коммит должен:

  • описывать что изменилось и зачем
  • быть небольшим и логически цельным
  • Часто используют стиль вроде:

  • feat: ... новая функциональность
  • fix: ... исправление
  • refactor: ... рефакторинг без изменения поведения
  • test: ... тесты
  • Отладка: как находить причину ошибки

    Начинаем с traceback и воспроизводимости

    Когда программа падает, Python печатает traceback. Ваша задача:

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

    print-отладка: допустимо, но ограниченно

    print() полезен, чтобы быстро проверить гипотезу, но в проектном коде он плохо масштабируется:

  • засоряет вывод
  • сложнее отключать и фильтровать
  • не даёт контекста (уровень, модуль, время)
  • Для устойчивой диагностики лучше использовать логирование.

    Встроенный отладчик pdb и breakpoint

    Python включает отладчик pdb. Документация: pdb.

    Самый удобный способ поставить точку остановки в современном Python это breakpoint():

    Команды внутри отладчика:

  • l показать код вокруг текущей строки
  • n шаг вперёд без захода внутрь вызова
  • s шаг с заходом внутрь
  • c продолжить выполнение
  • p expr вывести значение выражения
  • Практика: ставьте breakpoint() как можно ближе к месту, где данные становятся “не такими”.

    Частая причина багов: изменяемость и разделяемые ссылки

    Из прошлых тем: list, dict, set изменяемые. Очень частая ситуация:

  • объект передали в функцию
  • функция изменила его “на месте”
  • снаружи внезапно всё стало другим
  • При отладке проверяйте:

  • кто владеет объектом
  • где именно он изменился
  • не используется ли один и тот же объект в нескольких местах
  • Логирование: диагностировать систему, а не угадывать

    Почему логирование лучше print

    Логи дают управляемость:

  • уровни важности
  • единый формат
  • маршрутизацию (консоль, файл, система логов)
  • возможность отключить шум без изменения кода
  • Документация: logging.

    Уровни логов

    Типовые уровни:

  • DEBUG подробности для разработки
  • INFO нормальные события
  • WARNING подозрительные ситуации, но система работает
  • ERROR ошибка, операция не выполнена
  • CRITICAL система в критическом состоянии
  • Базовая настройка и использование

    Правильная обработка исключений в логах

    Когда вы ловите исключение и хотите записать traceback, используйте logger.exception() внутри except:

    Это сильно ускоряет диагностику, потому что сохраняется стек вызовов.

    Логи и безопасность

    Нельзя логировать:

  • пароли
  • токены
  • секретные ключи
  • персональные данные без необходимости и политики хранения
  • Практическое правило: логируйте идентификаторы и контекст, но не секреты.

    Тестирование: pytest как стандарт де-факто

    Зачем тесты нужны именно middle-разработчику

    Тесты нужны не “чтобы была галочка”, а чтобы:

  • безопасно рефакторить код
  • ловить регрессии
  • фиксировать требования к поведению
  • документировать систему через примеры
  • Официальная документация: pytest.

    Структура тестов и обнаружение

    pytest обычно ищет:

  • файлы test_.py или _test.py
  • функции test_*
  • Пример минимального теста:

    Запуск:

    Почему assert в pytest “особенный”

    pytest показывает разницу при падении assert и часто объясняет, что именно не совпало. Это одна из причин популярности pytest.

    Фикстуры: управление зависимостями теста

    Фикстура это функция, которая подготавливает данные или окружение для теста.

    Фикстуры особенно важны, когда вы дойдёте до Django:

  • база данных
  • клиент для запросов
  • настройки
  • !Как pytest строит зависимости и порядок выполнения фикстур

    Параметризация: один тест, много случаев

    Это помогает быстро покрывать граничные случаи.

    Моки и monkeypatch: изоляция внешних зависимостей

    Если код обращается к сети, времени, окружению, файловой системе, тесты могут стать нестабильными. Один из инструментов pytest это monkeypatch.

    Покрытие кода (coverage)

    Для оценки покрытия часто используют плагин pytest-cov: pytest-cov.

    Типичный запуск:

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

    Качество кода: форматирование, линтинг, типизация, pre-commit

    Форматтер: Black

    Black делает код единообразным и снимает споры “как форматировать”.

  • сайт проекта: Black
  • Запуск:

    Линтер: Ruff

    Ruff это быстрый линтер, который умеет ловить много ошибок и часто заменяет набор отдельных инструментов.

  • документация: Ruff
  • Запуск:

    Частые находки линтера:

  • неиспользуемые импорты
  • подозрительные сравнения
  • “мёртвый” код
  • ошибки стиля
  • Типизация: mypy

    Type hints помогают:

  • раньше находить ошибки на уровне интерфейсов функций
  • улучшать автодополнение и навигацию в IDE
  • проще рефакторить
  • Инструмент проверки типов: mypy.

    Пример аннотаций:

    Запуск:

    Практическое правило: типизация приносит пользу, когда вы применяете её к публичным границам модулей и ключевым функциям, а не пытаетесь мгновенно “протипизировать всё”.

    Автоматизация проверок: pre-commit

    Чтобы не забывать запускать форматтер, линтер и базовые проверки, используют git-хуки через pre-commit.

  • документация: pre-commit
  • Идея:

  • вы фиксируете набор проверок в конфиге
  • они автоматически запускаются перед коммитом
  • Это повышает качество и снижает количество “шумных” замечаний на ревью.

    Минимальный набор проверок для Python-проекта

    Частая базовая связка:

  • black для форматирования
  • ruff для линтинга
  • pytest для тестов
  • mypy для типов, если проект типизирован
  • Полезно договориться в команде:

  • какие инструменты обязательны
  • какие правила считаются ошибкой
  • что запускается в CI
  • Как собрать всё в один рабочий процесс

    Надёжный ежедневный цикл выглядит так:

  • Создать ветку под задачу.
  • Писать код небольшими логическими порциями.
  • Локально запускать ruff, black, pytest.
  • Делать маленькие коммиты с понятными сообщениями.
  • Открывать pull request и проходить ревью.
  • CI повторяет проверки и страхует от “работает у меня”.
  • Связь с остальными темами курса:

  • модули и пакеты дают структуру, которую легко тестировать и линтить
  • исключения и контекстные менеджеры помогают писать код, который предсказуемо логируется и корректно освобождает ресурсы
  • алгоритмическая база помогает не только “ускорять”, но и писать тесты на граничные случаи
  • Итоги

    Вы собрали набор инструментов, без которых невозможно уверенно работать как Python-разработчик среднего уровня:

  • Git как основа истории изменений, веток и командной работы
  • практики отладки через traceback и breakpoint()
  • логирование через logging с уровнями и корректной записью исключений
  • тестирование через pytest: assert, фикстуры, параметризация, изоляция окружения
  • качество кода через форматирование, линтинг, типизацию и автоматизацию проверок
  • Дальше, когда мы перейдём к Django и библиотекам, эти инструменты станут вашей “системой безопасности”: вы будете быстрее находить ошибки, увереннее менять код и проще поддерживать проект в команде.

    6. Работа с данными и интеграции: SQL, ORM, HTTP, REST, асинхронность, очереди и кэш

    Работа с данными и интеграции: SQL, ORM, HTTP, REST, асинхронность, очереди и кэш

    Зачем эта тема нужна для уровня middle

    Python-разработчик среднего уровня почти всегда работает не в вакууме: код читает и пишет данные, ходит в чужие сервисы по сети, отдаёт API, выполняет фоновые задачи, ускоряет горячие места кэшем. Ошибки и деградации производительности здесь часто возникают не из-за синтаксиса, а из-за неверных контрактов между компонентами.

    Эта тема связывает всё, что вы уже прошли:

  • из темы про модули и окружение вы берёте структуру проекта и управляемые зависимости
  • из темы про исключения и контекстные менеджеры вы берёте безопасную работу с ресурсами (соединения, транзакции)
  • из темы про алгоритмы и структуры данных вы берёте мышление про стоимость операций (индексы, количество запросов, кэш)
  • из темы про инструменты разработчика вы берёте тестирование, логирование и воспроизводимость
  • Цель статьи: дать единый практический слой по данным и интеграциям, чтобы вы уверенно читали и писали код вокруг баз данных, API, асинхронности, очередей и кэша.

    SQL как базовый язык данных

    Что такое реляционная база данных

    Реляционная база хранит данные в таблицах.

  • таблица состоит из строк и столбцов
  • строка обычно представляет сущность, например пользователя
  • столбцы представляют поля, например email, created_at
  • Ключевые понятия:

  • первичный ключ: уникальный идентификатор строки, часто id
  • внешний ключ: ссылка на строку в другой таблице
  • ограничения: правила целостности (например, NOT NULL, UNIQUE)
  • Ориентир по SQL и поведению конкретной СУБД лучше смотреть в документации вашей базы, например:

  • Документация PostgreSQL
  • Минимальный набор SQL, который должен знать middle

    #### DDL и DML

  • DDL описывает структуру: CREATE TABLE, ALTER TABLE
  • DML работает с данными: SELECT, INSERT, UPDATE, DELETE
  • Пример схемы:

    Пример запросов:

    #### JOIN: как соединять таблицы

    Если у вас есть users и orders, вы соединяете данные по ключам.

  • INNER JOIN возвращает только строки, где есть совпадение с обеих сторон
  • LEFT JOIN возвращает все строки слева и совпавшие справа (несовпавшие справа будут NULL)
  • Практическая мысль: умение читать JOIN напрямую влияет на то, насколько вы способны понимать, что реально делает ORM.

    Индексы: почему запросы внезапно становятся медленными

    Индекс ускоряет поиск по условию, но стоит памяти и замедляет запись.

    Типичные ситуации, где индекс нужен:

  • частая фильтрация WHERE email = ...
  • частая сортировка ORDER BY created_at
  • частая связка по внешнему ключу orders.user_id
  • В PostgreSQL почти всегда вы смотрите план выполнения:

  • PostgreSQL EXPLAIN
  • Ключевая идея для практики: не гадать, а проверять план и реальные времена.

    Транзакции: согласованность и конкуренция

    Транзакция объединяет несколько операций в единое целое.

  • либо применяются все изменения
  • либо не применяется ничего
  • Пример логики, которая должна быть атомарной:

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

    Концептуальные свойства обычно описывают через ACID.

  • атомарность
  • согласованность
  • изоляция
  • долговечность
  • Для Python-экосистемы важно: транзакции обычно оформляются контекстным менеджером.

    ORM: зачем он нужен и где подводные камни

    Что такое ORM

    ORM связывает объекты языка и таблицы БД.

  • вы описываете модель (класс) и её поля
  • ORM генерирует SQL и превращает результат обратно в объекты
  • Плюсы:

  • меньше шаблонного SQL в коде
  • удобные миграции схемы
  • единый стиль доступа к данным
  • Минусы:

  • легко не заметить, сколько SQL-запросов реально выполняется
  • сложные запросы иногда проще выразить на SQL
  • Lazy-evaluation и цена запроса

    Почти все ORM строят запросы лениво: пока вы не начали реально получать данные, SQL может не выполняться.

    Типовой симптом проблем: вроде один запрос, а в реальности их десятки.

    Проблема N+1

    Суть:

  • вы получаете список объектов (1 запрос)
  • затем для каждого объекта получаете связанную сущность (ещё N запросов)
  • Это часто превращает работу страницы или API-эндпоинта в серию мелких запросов.

    !Иллюстрация проблемы N+1 и как её устраняют

    Django ORM как основной пример для курса

    Официальная точка входа:

  • Документация Django: Models
  • Документация Django: Making queries
  • Ключевые вещи, которые важно понимать на уровне middle:

  • QuerySet ленивый: запрос выполняется при итерации, list(), len(), exists() и подобных операциях
  • select_related уменьшает количество запросов для связей типа один-к-одному и многие-к-одному
  • prefetch_related уменьшает количество запросов для связей типа многие-ко-многим и один-ко-многим
  • транзакции в Django оформляются через контекстный менеджер
  • Документация по оптимизации запросов:

  • Документация Django: QuerySet API
  • Практическое правило: когда что-то стало медленно, сначала посчитайте количество запросов и их время, а уже потом оптимизируйте.

    Когда уместен raw SQL

    Raw SQL уместен, когда:

  • запрос очень сложный и ORM делает его неочевидным
  • нужны специфичные конструкции вашей СУБД
  • нужна точная оптимизация под индексы и план
  • Важно: raw SQL не должен разрушать архитектуру. Обычно его изолируют в отдельном модуле репозитория и покрывают тестами.

    HTTP: базовый протокол интеграций

    Как устроен HTTP запрос и ответ

    HTTP это протокол запрос-ответ.

  • клиент отправляет метод, путь, заголовки и иногда тело
  • сервер отвечает статусом, заголовками и телом
  • !Структура HTTP запроса и ответа

    Официальная спецификация HTTP:

  • RFC 9110: HTTP Semantics
  • Методы и идемпотентность

    Ключевые методы:

  • GET получить ресурс
  • POST создать ресурс или запустить действие
  • PUT заменить ресурс целиком
  • PATCH частично изменить ресурс
  • DELETE удалить ресурс
  • В интеграциях важна идемпотентность.

  • идемпотентная операция при повторе даёт тот же результат
  • это важно для ретраев при сетевых сбоях
  • Практическое следствие:

  • повторять GET безопасно
  • повторять PUT обычно безопасно, если запрос одинаковый
  • повторять POST часто опасно без идемпотентных ключей, потому что можно создать дубль
  • Статусы ответа

    Минимальный набор:

  • 200 OK успех
  • 201 Created создано
  • 204 No Content успех без тела
  • 400 Bad Request ошибка валидации или запроса
  • 401 Unauthorized нет аутентификации
  • 403 Forbidden нет прав
  • 404 Not Found ресурс не найден
  • 409 Conflict конфликт состояния
  • 429 Too Many Requests лимиты
  • 500 Internal Server Error ошибка сервера
  • Таймауты, ретраи и устойчивость

    Почти любая сеть нестабильна, поэтому middle должен по умолчанию думать про:

  • таймауты на соединение и чтение
  • ретраи только там, где это корректно
  • ограничение параллелизма
  • деградацию при проблемах у зависимостей
  • Хорошая практическая база для клиентов:

  • Документация Requests
  • Документация HTTPX
  • Пример синхронного запроса с таймаутом:

    Ключевые мысли:

  • таймаут почти всегда нужно задавать явно
  • raise_for_status() превращает HTTP-ошибки в исключения, и их легче обрабатывать единообразно
  • REST API: как проектировать интеграции предсказуемо

    Что такое REST в прикладном смысле

    REST это набор архитектурных принципов. В разработке обычно подразумевают:

  • ресурсы (например, users, orders)
  • понятные URL
  • использование HTTP-методов по смыслу
  • stateless: сервер не должен полагаться на состояние клиента между запросами
  • Важно: REST не означает, что вы обязаны делать только CRUD. Это означает, что интерфейс должен быть предсказуемым.

    Практические правила проектирования

  • использовать существительные во множественном числе: /users/, /orders/
  • отдавать корректные статусы (201 при создании, 204 при удалении)
  • валидировать вход и возвращать понятные ошибки
  • поддерживать пагинацию и фильтры для списков
  • Пример пагинации как идеи:

  • GET /users?limit=50&offset=100
  • Версионирование API

    Когда API живёт долго, изменения должны быть управляемыми.

    Частые подходы:

  • версия в URL: /api/v1/...
  • версия в заголовке: Accept: application/vnd.company.v1+json
  • Важно не то, какой подход вы выберете, а то, что версия есть и она согласована в команде.

    Асинхронность в Python: когда она действительно нужна

    Синхронный и асинхронный код

    Асинхронность в Python решает прежде всего задачи ввода-вывода:

  • много HTTP-запросов
  • работа с сокетами
  • ожидание внешних сервисов
  • Она не делает CPU-задачи быстрее сама по себе.

    Официальная точка входа:

  • Документация Python: asyncio
  • Базовые элементы asyncio

  • async def объявляет корутину
  • await ждёт результата асинхронной операции
  • event loop планирует выполнение корутин
  • asyncio.gather запускает несколько задач параллельно с точки зрения ожидания
  • Пример конкурентных запросов через HTTPX:

    Обратите внимание на связку с предыдущими темами:

  • async with это тот же протокол контекстных менеджеров, только асинхронный
  • исключения так же поднимаются и обрабатываются, их нельзя игнорировать
  • Ограничение параллелизма

    Если запустить тысячи запросов сразу, вы можете:

  • перегрузить свой сервис
  • получить блокировки на стороне зависимости
  • упереться в лимиты ОС
  • Типичный инструмент: asyncio.Semaphore.

    Частая ошибка: блокирующие операции внутри async

    Если внутри async def вы делаете блокирующий вызов, вы тормозите весь event loop.

    Если очень нужно вызвать блокирующий код, выносите его в отдельный поток:

  • Документация Python: asyncio.to_thread
  • Очереди задач: фоновые работы и надёжность

    Зачем нужны очереди

    Очереди применяют, когда:

  • задача длительная и не должна держать HTTP-запрос
  • задача должна быть повторена при временной ошибке
  • нужно распределить нагрузку между воркерами
  • Примеры:

  • отправка email
  • генерация отчёта
  • обработка изображений
  • синхронизация с внешним сервисом
  • Архитектура очереди

    Обычно есть:

  • producer: кладёт сообщения
  • broker: хранит и доставляет сообщения
  • worker: забирает и выполняет
  • !Базовая схема очередей задач

    Популярные инструменты:

  • Документация Celery
  • Документация RQ
  • Важные свойства фоновых задач

  • acknowledgement: подтверждение обработки, чтобы сообщение не потерялось
  • retries: повторные попытки с задержкой
  • idempotency: возможность безопасно выполнить задачу повторно
  • dead letter: отдельное место для сообщений, которые постоянно падают
  • Практическое правило: если задача может выполниться дважды, система должна оставаться корректной.

    Кэш: ускорение чтения и защита базы

    Зачем кэшировать

    Кэш используют, чтобы:

  • снизить задержку ответа
  • уменьшить нагрузку на базу и внешние сервисы
  • переживать пики трафика
  • Популярные инструменты:

  • Документация Redis
  • Документация Django: Cache framework
  • Базовый паттерн cache-aside

    Идея:

  • при чтении сначала смотрим в кэш
  • при промахе читаем из источника истины (БД), кладём в кэш, возвращаем
  • Пример псевдокода:

    Ключевые тонкости:

  • TTL должен соответствовать бизнес-смыслу данных
  • формат ключей должен быть единообразным
  • сериализация должна быть согласована (JSON, msgpack, pickle с осторожностью)
  • Инвалидация: самая сложная часть кэширования

    Основные подходы:

  • TTL и автоматическое протухание
  • явная инвалидация при изменении данных
  • Типичный компромисс:

  • ставить небольшой TTL
  • дополнительно инвалидировать ключи при изменениях, если это критично
  • Cache stampede

    Если ключ протух, и одновременно приходит много запросов, все они могут пойти в БД.

    Типичные защиты:

  • блокировка на пересчёт ключа
  • джиттер TTL, чтобы ключи не протухали синхронно
  • предварительный прогрев
  • Интеграции как система: контракты, ошибки, наблюдаемость

    Контракты и форматы данных

    В интеграциях критично договориться о:

  • форматах (application/json)
  • обязательных и необязательных полях
  • допустимых статусах и ошибках
  • Чем лучше контракт, тем проще тестирование.

    Ошибки и повторные попытки

    Ретраи должны быть осмысленными:

  • повторяем только то, что безопасно повторять
  • различаем ошибки клиента (4xx) и ошибки сервера (5xx)
  • используем экспоненциальную задержку и ограничение числа попыток
  • Логирование и трассировка

    Из темы про логирование берите привычку:

  • логировать контекст (идентификаторы запросов, user_id, order_id)
  • не логировать секреты
  • писать ошибки с traceback
  • Итоги

    Теперь у вас есть связная база по данным и интеграциям на уровне, который ожидают от middle:

  • SQL как фундамент: SELECT, JOIN, индексы, транзакции
  • ORM как инструмент, который ускоряет разработку, но требует контроля числа запросов и понимания N+1
  • HTTP и REST как базовый язык общения сервисов: методы, статусы, таймауты, идемпотентность
  • асинхронность для задач ввода-вывода: async/await, конкурентность, ограничение параллелизма
  • очереди для фоновых задач и надёжной обработки: broker, worker, ретраи, идемпотентность
  • кэш как ускоритель: cache-aside, TTL, инвалидация и защита от stampede
  • Следующий логичный шаг в курсе после этой темы обычно — применить всё это внутри веб-фреймворка: Django (ORM, транзакции, кэш, фоновые задачи), а также научиться проектировать API и тестировать интеграции системно.

    7. Django до Middle: архитектура, модели, миграции, DRF, auth, безопасность, деплой и production

    Django до Middle: архитектура, модели, миграции, DRF, auth, безопасность, деплой и production

    Зачем эта тема нужна для уровня middle

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

    На уровне middle от вас ожидают, что вы:

  • понимаете архитектуру Django-проекта и границы ответственности компонентов
  • уверенно проектируете модели и связи, контролируете запросы и транзакции
  • умеете безопасно вести миграции и изменения схемы в командной разработке
  • строите и поддерживаете API на Django REST Framework
  • используете auth правильно и расширяете его без боли
  • учитываете безопасность по умолчанию и понимаете, от чего она защищает
  • умеете собрать приложение в production: настройки, статика, сервер приложений, логирование
  • Связь с предыдущими темами курса:

  • из темы про модули, пакеты и окружение вы берёте структуру проекта и управляемые зависимости
  • из темы про исключения и контекстные менеджеры — транзакции, корректное освобождение ресурсов, осмысленную обработку ошибок
  • из темы про алгоритмы и структуры данных — мышление про стоимость операций, особенно в БД и при работе с QuerySet
  • из темы про инструменты разработчика — тестирование, логирование, качество кода и CI
  • из темы про SQL/ORM/HTTP/REST/кэш/очереди — базу для правильного использования ORM, REST и интеграций в веб-приложении
  • Официальные источники, к которым стоит привыкнуть:

  • Документация Django
  • Документация Django REST Framework
  • Архитектура Django: проект, приложения и жизненный цикл запроса

    Проект и приложения

    Django различает:

  • проект — набор настроек и точка сборки приложения
  • приложения — изолированные модули бизнес-логики (например, users, billing, catalog)
  • Практический смысл для уровня middle:

  • приложение должно быть достаточно автономным: модели, сервисы, API, тесты внутри
  • “общий код” выносится в отдельные приложения или пакеты (common, core), но без свалки
  • Типичная структура:

    Документация: Приложения в Django

    MTV и запрос-ответ

    В Django часто говорят MTV:

  • Model — данные и правила работы с ними
  • Template — представление (HTML)
  • View — обработчик запроса, который формирует ответ
  • Важно не путать: Django view — это скорее контроллер из MVC.

    Жизненный цикл запроса упрощённо:

  • Веб-сервер (например, Nginx) принимает HTTP-запрос
  • Сервер приложений (Gunicorn/Uvicorn) передаёт запрос Django
  • Django прогоняет запрос через middleware
  • URL dispatcher выбирает view
  • View выполняет бизнес-логику (часто обращается к БД через ORM)
  • Возвращается HttpResponse (HTML/JSON/файл)
  • Ответ снова проходит через middleware и уходит клиенту
  • !Как запрос проходит через Django и где находятся ключевые точки расширения

    Документация:

  • Middleware
  • URL dispatcher
  • WSGI и ASGI

    Django поддерживает два интерфейса:

  • WSGI — классический синхронный веб
  • ASGI — асинхронный интерфейс (WebSocket, long-polling, async views)
  • В проекте вы увидите wsgi.py и asgi.py. Выбор сервера:

  • под WSGI обычно используют Gunicorn
  • под ASGI часто используют Uvicorn или Daphne
  • Документация:

  • Deployment: WSGI
  • Deployment: ASGI
  • Модели и ORM: проектирование данных на уровне middle

    Модель как контракт данных

    Модель Django — это описание таблицы и связей.

    Ключевые решения, которые отличают middle:

  • правильный выбор типа поля (EmailField, DecimalField для денег)
  • корректная политика удаления через on_delete
  • ограничения целостности (unique=True, UniqueConstraint)
  • индексы под реальные запросы
  • Документация: Model field reference

    Связи и их цена

    Базовые связи:

  • ForeignKey — многие к одному
  • OneToOneField — один к одному
  • ManyToManyField — многие ко многим
  • Важно: связь — это не только “удобно”, это ещё и стоимость запросов.

    Документация: Relationships

    QuerySet ленивый: когда реально выполняется SQL

    QuerySet строит запрос, но не выполняет его сразу. SQL обычно выполняется при:

  • итерации (for x in qs)
  • приведении к списку (list(qs))
  • len(qs)
  • exists(), first(), count()
  • Это важно, потому что легко получить “скрытые” запросы в неожиданных местах.

    Документация: QuerySet API

    N+1 и оптимизация запросов

    Проблема N+1 выглядит так:

  • вы получили список Book (1 запрос)
  • для каждой книги отдельно читается author (ещё N запросов)
  • Решение:

  • select_related для ForeignKey и OneToOneField
  • prefetch_related для ManyToManyField и обратных связей
  • Пример:

    Документация:

  • select_related
  • prefetch_related
  • Транзакции

    Транзакции нужны, когда несколько операций должны быть атомарными.

    В Django это обычно делается так:

    transaction.atomic() — контекстный менеджер: он гарантирует, что при исключении изменения откатятся.

    Документация: Database transactions

    Валидация: модель, форма, сериализатор

    В Django есть несколько уровней валидации:

  • на уровне БД: NOT NULL, UNIQUE, constraints
  • на уровне модели: clean(), валидаторы
  • на уровне форм: Form/ModelForm
  • на уровне API: DRF serializers
  • Middle-разработчик выбирает уровень валидации осознанно:

  • критически важную целостность фиксирует в БД (чтобы защититься от любых источников записи)
  • бизнес-валидацию делает ближе к входу (форма/сериализатор), чтобы возвращать понятные ошибки
  • Миграции: безопасная эволюция схемы

    Что такое миграции и почему это важно

    Миграции — это история изменений схемы базы данных.

    Рабочий цикл:

    Документация: Migrations

    Правила миграций для командной разработки

  • Не редактируйте уже применённые миграции в общей ветке
  • Любое изменение модели должно сопровождаться миграцией
  • Проверяйте миграции на чистой базе и на базе с данными
  • Разделяйте изменение схемы и миграцию данных, если это упрощает откаты и контроль
  • Миграции данных

    Иногда нужно изменить не только структуру, но и значения. Для этого используют RunPython.

    Ключевая деталь: в миграциях нельзя импортировать модель напрямую из models.py, нужно брать её через apps.get_model, чтобы миграция была воспроизводимой относительно состояния на момент её создания.

    Опасные изменения схемы

    Изменения, которые требуют внимания (особенно на больших таблицах):

  • добавление NOT NULL поля без значения по умолчанию
  • изменение типа поля (может быть дорогой операцией)
  • добавление уникальности на грязные данные
  • Практика middle-уровня:

  • планировать миграции с учётом данных
  • при необходимости делать изменения в несколько шагов (например, добавить nullable поле, заполнить, потом сделать not null)
  • DRF: API до уровня production

    Django REST Framework (DRF) — стандарт де-факто для REST API в Django.

    Документация: DRF: Quickstart

    Serializers: схема, валидация и преобразование

    Сериализатор отвечает за:

  • преобразование модели в JSON
  • чтение входных данных
  • валидацию
  • Важно различать:

  • validate_<field> — валидация одного поля
  • validate — валидация зависимостей между полями
  • Документация: Serializer validation

    Views и ViewSets

    На практике часто используют ModelViewSet, чтобы быстро получить CRUD.

    Важная мысль: даже если DRF даёт быстрый старт, middle обязан контролировать:

  • что возвращается наружу (не утекли ли приватные поля)
  • сколько запросов делает эндпоинт
  • какие права нужны
  • Документация: ViewSets

    Router и URL

    Документация: Routers

    Пагинация, фильтрация, версии

    Минимум, который обычно нужен в production API:

  • пагинация списков
  • фильтры
  • версионирование
  • Документация:

  • Pagination
  • Filtering
  • Versioning
  • Тестирование DRF

    Для тестов DRF используют APIClient.

    Документация: Testing

    Auth: пользователи, сессии, права доступа

    Встроенная система аутентификации Django

    Django включает:

  • модель пользователя
  • хэширование паролей
  • сессии
  • механизмы permissions и groups
  • Документация:

  • Authentication
  • Password management
  • Пользовательская модель User

    Практическое правило: если вы планируете кастомизацию пользователя (email вместо username, дополнительные поля), задайте свою модель сразу, в начале проекта.

    Это делается через AUTH_USER_MODEL.

    Документация: Customizing authentication in Django

    Почему “сразу” важно:

  • менять модель пользователя в уже живом проекте сложно из-за миграций и связей
  • Auth в DRF: аутентификация и разрешения

    В DRF обычно разделяют:

  • authentication — кто пользователь
  • permissions — что ему можно
  • Документация:

  • Authentication
  • Permissions
  • В production часто используют:

  • session auth для браузерных клиентов
  • token-based подходы для API
  • Если вы используете JWT, выбирайте поддерживаемые библиотеки и внимательно относитесь к срокам жизни токенов и ротации ключей.

    Безопасность: что включено по умолчанию и что легко сломать

    Ключевые настройки

    Минимальные обязательные правила:

  • DEBUG = False в production
  • уникальный SECRET_KEY, не хранить его в репозитории
  • корректный ALLOWED_HOSTS
  • Документация:

  • Deployment checklist
  • CSRF

    CSRF — атака, когда браузер пользователя отправляет запрос от его имени на сайт, где он уже авторизован.

    Django защищает формы и session-based запросы CSRF-токеном.

    Документация: CSRF protection

    XSS

    Django templates по умолчанию экранируют переменные, что снижает риск XSS. Риск появляется, когда вы:

  • отключаете экранирование
  • вставляете непроверенный HTML
  • отдаёте пользовательский контент без очистки
  • Документация: Security in Django

    SQL-инъекции

    Django ORM параметризует запросы, поэтому “классические” SQL-инъекции обычно предотвращаются.

    Риск появляется, когда вы:

  • строите raw SQL через конкатенацию строк
  • используете небезопасные фрагменты в extra() или сырой SQL без параметров
  • Документация: Database security

    HTTPS, cookies и заголовки безопасности

    В production обычно включают:

  • SECURE_SSL_REDIRECT
  • флаги cookies: SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY
  • HSTS, если готовы поддерживать HTTPS всегда
  • Документация: Security middleware

    Секреты и конфигурация

    Практика:

  • хранить секреты в переменных окружения
  • разделять настройки по окружениям (dev/stage/prod)
  • не логировать токены, пароли и персональные данные
  • Деплой и production: как довести Django до работающего сервиса

    Минимальная production-архитектура

    Обычно есть три слоя:

  • reverse proxy (часто Nginx): TLS, статика, лимиты
  • сервер приложений (Gunicorn/Uvicorn): воркеры
  • Django-приложение + база данных
  • !Базовая production-схема: веб, приложение, база, кэш и фоновые задачи

    Статика и медиа

    Различайте:

  • static — файлы приложения (CSS/JS), собираются командой collectstatic
  • media — пользовательские загрузки
  • Типичный порядок:

    Документация: Managing static files

    Миграции в деплое

    Частая практика:

  • миграции запускаются как отдельный шаг при выкладке
  • если миграция потенциально тяжёлая, её планируют и тестируют отдельно
  • Логирование и наблюдаемость

    Для production важно:

  • структурированные логи (хотя бы единый формат)
  • запись traceback при ошибках
  • корреляция запросов (request id), если система распределённая
  • Django даёт встроенную систему логирования.

    Документация: Logging

    Производительность: кэш, фоновые задачи, “узкие места”

    Ускорение Django-проекта обычно идёт по слоям:

  • уменьшить число запросов (N+1, лишние count(), повторные обращения)
  • добавить индексы под реальные фильтры
  • кэшировать горячие чтения (Django cache framework)
  • вынести тяжёлые операции в фоновые задачи
  • Документация:

  • Django cache framework
  • Типичный набор production-настроек

    | Область | Минимум, который должен быть настроен | |---|---| | Конфигурация | DEBUG=False, ALLOWED_HOSTS, секреты вне репозитория | | Безопасность | SecurityMiddleware, secure cookies, HTTPS | | База | PostgreSQL, корректные таймауты, пуллинг по необходимости | | Статика | collectstatic, раздача Nginx или CDN | | Ошибки | логирование, сбор ошибок (например, Sentry) | | Тесты | прогон в CI перед деплоем |

    Если вы подключаете Sentry, используйте официальную документацию SDK:

  • Sentry Python SDK
  • Практический чеклист “Django до middle”

  • Вы уверенно объясняете, как запрос проходит через middleware, urls и view
  • Вы проектируете модели с учётом связей, ограничений и будущих запросов
  • Вы умеете находить и устранять N+1 через select_related и prefetch_related
  • Вы делаете миграции безопасно и понимаете, когда нужна миграция данных
  • Вы строите API на DRF с валидацией, правами, пагинацией и тестами
  • Вы понимаете различие authentication и permissions, и корректно выбираете механизм
  • Вы не ломаете безопасность настройками и понимаете угрозы CSRF/XSS/секретов
  • Вы знаете базовую production-сборку: сервер приложений, статика, миграции, логи
  • Эта база позволяет перейти от “я сделал CRUD” к “я поддерживаю Django-сервис в команде и production”.