В этой функции баг: 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 в дефолтах ведут себя так же