Поведение мутабельного аргумента по умолчанию в Python

Поведение мутабельного аргумента по умолчанию в Python

@python_quiz

Разберем этот квиз

Кратко: функция использует список как аргумент по умолчанию, и этот объект сохраняется между вызовами. Это даёт неожиданные побочные эффекты, если ожидать, что каждый вызов получает «чистый» пустой список.

Исходный фрагмент кода

python

Что делает код:

  • При n > 0 функция добавляет текущее n в список acc, затем рекурсивно вызывает себя с n-1 и тем же списком.
  • Поскольку acc.append(n) возвращает None, выражение использует or для вызова рекурсии и в итоге возвращает результат рекурсивного вызова.
  • Список acc по умолчанию создаётся один раз при определении функции и переиспользуется между вызовами без передачи явного аргумента.

Пошаговый разбор выполнения

  1. Первый вызов f(2):

- acc по умолчанию — [].
- acc.append(2) → acc = [2]
- рекурсивный вызов f(1, [2]):
- acc.append(1) → acc = [2, 1]
- рекурсивный вызов f(0, [2, 1]) возвращает acc → [2, 1]
- Возвращается [2, 1] и печатается.

  1. Второй вызов f(1):

- Поскольку аргумент по умолчанию тот же объект acc, он уже равен [2, 1].
- acc.append(1) → acc = [2, 1, 1]
- Рекурсия приводит к возврату [2, 1, 1], и это печатается.

Итого, программа выведет:

python

Анализ вариантов ответа (строки как в квизе)

  • "[2, 1] и [2, 1, 1]"

Соответствует описанному поведению — первый вызов создал [2, 1], второй добавил ещё один 1 в тот же список.

  • "[2, 1] и [1]"

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

  • "[2, 1] и [2, 1]"

Такое произошло бы, если бы второй вызов не мутировал список или если бы второй вызов использовал копию. В текущем коде список мутирует снова.

  • "[2, 1] и [1, 1]"

Невозможный результат для данного кода: второй вызов стартует с уже имеющегося [2,1], добавляет 1 и получает [2,1,1], а не [1,1].

Как исправить (без побочных эффектов)

Рекомендованный паттерн — не использовать мутабельный объект как значение по умолчанию. Вместо этого применяют None и создают новый список внутри функции:

python

Теперь для каждого вызова без явного аргумента acc создаётся новый список, поэтому второй вызов не видит изменений, сделанных в первом.

Выводы и рекомендации

  • Аргументы по умолчанию вычисляются один раз при определении функции. Если это мутабельный объект (список, словарь и т. п.), он будет общим для всех вызовов функции, которые не передают этот аргумент явно.
  • Избегайте использования мутабельных объектов в качестве дефолтов. Используйте None (или другой неизменяемый тип) и создавайте новый объект внутри функции.
  • Такой паттерн особенно важен в рекурсии или при аккумулирующих функциях, чтобы избежать трудноуловимых багов, связанных с сохраняемым состоянием.

Небольшая шпаргалка:

python

Report Page