← Назад к списку
ПрограммированиеPythonJunior

В этой функции баг: def add_item(item, items=[]) — объясните его и исправьте.

Короткий ответ

  • Значение по умолчанию вычисляется один раз при определении
  • Один и тот же список переживает все вызовы функции
  • Элементы «накапливаются» между независимыми вызовами
  • Исправление: items=None и создание списка внутри
  • То же касается dict, set и любых изменяемых объектов
  • Линтеры (ruff B006) ловят эту ошибку автоматически

Изменяемое значение по умолчанию создаётся один раз и разделяется между вызовами; правильный шаблон — None по умолчанию и создание объекта в теле.

Как сказать вслух

пример ответа

Значения по умолчанию в Python вычисляются один раз — в момент определения функции, а не при каждом вызове. Поэтому пустой список здесь общий для всех вызовов: добавили элемент в первом вызове — он останется и во втором. Исправляется стандартным шаблоном: по умолчанию ставим None, а внутри функции создаём новый список, если аргумент не передали.

Подробный ответ

Основной ответ

Выражения в значениях по умолчанию исполняются один раз при выполнении инструкции def, и результат сохраняется в атрибуте функции __defaults__. Для неизменяемых значений (числа, строки, None) это безвредно, но изменяемый объект — список, словарь, множество — становится общим состоянием: каждый вызов без аргумента получает тот же самый объект, и мутации накапливаются. Это проявляется как «функция помнит прошлые вызовы». Канонический фикс — сторожевое значение: items=None и items = [] if items is None else items в теле. Если None — допустимое значение аргумента, используют собственный sentinel-объект. Иногда поведение используют намеренно как кэш, но для этого честнее functools.lru_cache.

Ключевые моменты

  • Момент вычисления. def — исполняемая инструкция; дефолты считаются при ней один раз и живут в __defaults__ функции.
  • Шаблон исправления. None как дефолт плюс создание нового объекта в теле — новый список на каждый вызов.
  • Sentinel. Когда None — легальное значение, заводят приватный маркер _missing = object() и сравнивают через is.
  • Автоматическое обнаружение. Правило B006 в ruff/flake8-bugbear помечает изменяемые дефолты ещё на ревью.

Практический контекст

Это одна из самых частых задач-ловушек на собеседованиях уровня junior и middle: кандидату показывают код и просят предсказать вывод двух вызовов. В реальных проектах баг встречается в хелперах с аккумуляторами и в дефолтных конфигах-словарях, а проявляется редко и недетерминированно, поэтому его важно узнавать мгновенно.

Пример кода

def add_item_buggy(item, items=[]):
    items.append(item)
    return items

print(add_item_buggy(1))  # [1]
print(add_item_buggy(2))  # [1, 2] — список общий!

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [2]

Частые ошибки

  • Объясняют баг «особенностью интерпретатора», не упоминая момент вычисления дефолтов
  • Исправляют через items=list() — это тот же изменяемый дефолт, вычисленный однажды
  • Забывают, что dict и set в дефолтах ведут себя так же

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru