Сначала воспроизведите дефект, затем внесите минимальное исправление и проверьте его. Разбор откройте после своей попытки. Данные в задачах учебные.
Как выбрать практику · Карта промахов
Решать с агентом
Скопируйте этот текст и добавьте условие выбранного задания:
Ты мой ревьюер по 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)
Усложнение: Добавьте смену команды пользователя и проверьте старый доступ.