Мутабельный аргумент по умолчанию в Python

Мутабельный аргумент по умолчанию в Python

@python_quiz

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

Кратко: в Python объекты, указанные в сигнатуре как значения по умолчанию (например, L=[]), создаются один раз — при определении функции — и затем переиспользуются при каждом вызове. Это часто приводит к неожиданному поведению, когда значение по умолчанию — изменяемый объект (list, dict и т.д.).

Условие — код из квиза

python

Что происходит при выполнении этого кода?

Поведение и объяснение

При определении функции объект списка для параметра L создаётся один раз и остаётся тем же самым между вызовами. Поэтому:

  • Первый вызов f(1) добавляет 1 в список по умолчанию и возвращает [1].
  • Второй вызов f(2) использует тот же список, добавляет 2 и возвращает [1, 2].

Вывод будет:

python

Это не ошибка и не неожиданность с точки зрения механизма языка — просто нужно помнить про семантику значений по умолчанию.

Демонстрация того, что список один и тот же (идентичность объекта)

python

Типичный вывод:

Заметьте: id(L) совпадает — это один и тот же объект.

Разбор вариантов (почему другие предположения неверны)

  • Идея "выведет [1] затем [2]": предполагает, что новый список создаётся при каждом вызове. Это верно для неизменяемых значений по умолчанию (или при явном создании нового объекта внутри функции), но не для мутируемых значений, указанных в сигнатуре.
  • Идея "выведет [1, 2] затем [1, 2]": требует, чтобы список уже содержал оба элемента до первого вывода — такого не происходит при приведённом коде.
  • Идея "произойдёт ошибка из-за мутабельного аргумента": Python не запрещает изменяемые объекты как значения по умолчанию; ошибка здесь не возникает.

Как получить «ожидаемое» поведение (новый список при каждом вызове)

Обычно используется паттерн со значением по умолчанию None:

python

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

Когда полезно текущее поведение

Иногда намеренно хотят сохранить состояние между вызовами (например, кэширование, накопление значений). В этом случае мутируемый объект по умолчанию может быть использован сознательно. Но лучше в таких ситуациях явно документировать поведение или использовать более явные конструкции (атрибут функции, класс, замыкание):

Пример с аккумулирующей функцией через атрибут:

python

Резюме и рекомендации

  • Помните: выражения в сигнатуре функции (включая списки и словари) вычисляются один раз при определении функции.
  • Не используйте изменяемые объекты в качестве значений по умолчанию, если ожидаете, что при каждом вызове будет новый объект.
  • Для «нового» объекта при каждом вызове используйте None и создавайте объект внутри функции.
  • Если же вам нужно сохранять состояние между вызовами — делайте это сознательно и явно (атрибут функции, класс, замыкание), чтобы код был понятен другим разработчикам.

Report Page