Материал

Найди и исправь: практика ревью

Раздел практикиПрактика по форматам →

Сначала воспроизведите дефект, затем внесите минимальное исправление и проверьте его. Разбор откройте после своей попытки. Данные в задачах учебные.

Как выбрать практику · Карта промахов

Решать с агентом

Скопируйте этот текст и добавьте условие выбранного задания:

Ты мой ревьюер по backend. Не исправляй код за меня. Сначала попроси воспроизвести дефект и назвать причину, затем попроси минимальный патч и проверку. После моего решения сравни его с критериями, укажи оставшиеся крайние случаи и только затем покажи свой вариант. По запросу давай одну подсказку за раз.

Прокрутите таблицу по горизонтали →

Задание Этап Ориентир
Уведомления разных посетителей смешались Базовый Python 20 мин
Среднее по потоку пропало Инженерный Python 25 мин
Пустой зал получил одну бронь SQL и PostgreSQL 20 мин
Последнее место продано дважды Web-фреймворк 30 мин
Чужая инструкция видна по идентификатору Web-фреймворк 25 мин

Уведомления разных посетителей смешались

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

def add_notice(message, history=[]):
    history.append(message)
    return history

Проверка результата

  • Без переданного списка второй вызов не видит уведомления первого.
  • Явно переданный список дополняется и возвращается по согласованному контракту.
  • Тест ловит проблему при повторных вызовах, а не только при одном.
Подсказка — после своей попытки

Когда создаётся список, записанный в заголовке функции?

Разбор — после своей попытки

Значение аргумента по умолчанию создаётся один раз. Используйте None как маркер и создавайте список внутри вызова; если передан список, дополняйте именно его.

Один вариант решения:

def add_notice(message, history=None):
    if history is None:
        history = []
    history.append(message)
    return history

Усложнение: Сравните исправление через None с вариантом, который копирует переданный список; выберите контракт явно.

Вернуться к этапу

Среднее по потоку пропало

Функция работает со списком задержек, но ломается на одноразовом генераторе. Найдите причину и исправьте её за один проход. Для пустого входа договоритесь выдавать ValueError.

def average_delay(delays):
    count = sum(1 for _ in delays)
    return sum(delays) / count

Проверка результата

  • Один и тот же результат получается для списка и одноразового генератора с одинаковыми значениями.
  • Пустой вход обрабатывается явной ошибкой.
  • Решение не создаёт копию всего потока и проходит его один раз.
Подсказка — после своей попытки

Что осталось в генераторе после вычисления `count`?

Разбор — после своей попытки

Первый sum исчерпывает генератор. Копите сумму и количество в одном цикле; для нулевого количества выбрасывайте ValueError.

Один вариант решения:

def average_delay(delays):
    total = 0
    count = 0
    for delay in delays:
        total += delay
        count += 1
    if count == 0:
        raise ValueError('no delays')
    return total / count

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

Вернуться к этапу

Пустой зал получил одну бронь

В таблице rooms есть зал 7, а броней у него нет. Запрос сообщает одну бронь. Исправьте счётчик так, чтобы все залы остались в отчёте.

SELECT r.id, COUNT(*) AS bookings
FROM rooms AS r
LEFT JOIN bookings AS b ON b.room_id = r.id
GROUP BY r.id;

Проверка результата

  • Зал без броней получает 0, зал с двумя бронями — 2.
  • Объяснена разница между COUNT(*) и COUNT(b.id) после LEFT JOIN.
  • Проверка сохраняет зал без связанных строк.
Подсказка — после своей попытки

Сколько строк создаёт `LEFT JOIN` для зала без броней?

Разбор — после своей попытки

LEFT JOIN оставляет одну строку зала с NULL справа. COUNT(*) считает эту строку; COUNT(b.id) считает только существующие брони.

Один вариант решения:

SELECT r.id, COUNT(b.id) AS bookings
FROM rooms AS r
LEFT JOIN bookings AS b ON b.room_id = r.id
GROUP BY r.id;

Усложнение: Добавьте статус брони; аккуратно проверьте, где поставить фильтр, чтобы залы не пропали.

Вернуться к этапу

Последнее место продано дважды

Два запроса одновременно читают available = 1, затем оба продают место. Найдите гонку и исправьте операцию так, чтобы ровно один запрос получил успех. Для задачи считайте, что db.fetchval и db.execute выполняют отдельные SQL-команды.

async def book(seat_id, db):
    available = await db.fetchval("SELECT available FROM seats WHERE id = $1", seat_id)
    if available is None or available == 0:
        return False
    await db.execute("UPDATE seats SET available = available - 1 WHERE id = $1", seat_id)
    return True

Проверка результата

  • При двух конкурентных вызовах с одним местом успех получает ровно один.
  • Число мест не становится отрицательным.
  • Отсутствующий seat_id возвращает False; объяснено, как определить успех обновления.
Подсказка — после своей попытки

Проверку остатка можно сделать частью самого `UPDATE`.

Разбор — после своей попытки

Отдельные чтение и обновление дают гонку. Используйте один UPDATE ... WHERE id = $1 AND available > 0 RETURNING available; наличие возвращённой строки означает успех.

Один вариант решения:

async def book(seat_id, db):
    remaining = await db.fetchval(
        'UPDATE seats SET available = available - 1 '
        'WHERE id = $1 AND available > 0 '
        'RETURNING available',
        seat_id,
    )
    return remaining is not None

Усложнение: Добавьте отмену брони и проверьте, что она не увеличивает остаток дважды.

Вернуться к этапу

Чужая инструкция видна по идентификатору

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

async def load_manual(manual_id, user, db):
    row = await db.fetchrow(
        "SELECT id, team_id, title FROM manuals WHERE id = $1", manual_id
    )
    if row is None:
        return None
    return dict(row)

Проверка результата

  • Пользователь своей команды получает свой документ.
  • Чужой и отсутствующий документ не возвращают данные; ответ для них одинаковый.
  • Права проверяются сервером в запросе, а не только клиентским фильтром.
Подсказка — после своей попытки

Как сделать `team_id` пользователя частью условия поиска?

Разбор — после своей попытки

Фильтр только по id не ограничивает владельца. Добавьте AND team_id = $2 с доверенным user.team_id; одинаково обработайте чужую и отсутствующую строку.

Один вариант решения:

async def load_manual(manual_id, user, db):
    row = await db.fetchrow(
        'SELECT id, team_id, title FROM manuals '
        'WHERE id = $1 AND team_id = $2',
        manual_id,
        user.team_id,
    )
    return None if row is None else dict(row)

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

Вернуться к этапу