Поведение mutable default аргументов в Python — коротко и наглядно

Поведение mutable default аргументов в Python — коротко и наглядно

@python_quiz

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

Код из квиза:

python

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

text

Почему так происходит — разберёмся.

Ключевые идеи

  • Параметр со значением по умолчанию вычисляется один раз — при определении функции. В данном случае lst=[] создаёт один и тот же список, который будет переиспользоваться при каждом вызове без явного второго аргумента.
  • Списки в Python изменяемы (mutable). Вызов lst.append(x) меняет содержимое этого единственного списка.
  • Аргументы в вызове print(f(1), f(2)) вычисляются слева направо: сначала выполняется f(1), потом f(2). Но оба вызова работают с одним и тем же списком. Когда f(1) вернул ссылку на список [1], затем f(2) добавил 2 в тот же список — и к моменту, когда print выводит значения, оба аргумента ссылаются на список [1, 2].

Пошаговый ход выполнения

  1. При определении функции создаётся объект списка, привязанный к параметру по умолчанию lst.
  2. Выполняется f(1):

- lst.append(1) — общий список становится [1].
- f(1) возвращает ссылку на этот списка (т.е. объект со значением [1]).

  1. Выполняется f(2):

- Используется тот же объект lst.
- lst.append(2) — список теперь [1, 2].
- f(2) возвращает ту же ссылку (объект [1, 2]).

  1. print получает две ссылки на один и тот же объект и выводит его текущее содержимое: [1, 2] [1, 2].

Демонстрация в интерактивном виде

python

Вывод:

Как избежать неожиданных эффектов

Если вы хотите, чтобы при каждом вызове создавался новый список по умолчанию, используйте None и создавайте список внутри тела функции:

python

Теперь:

python

выведет:

Другие варианты безопасного поведения:

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

Когда это полезно и когда опасно

  • Польза: можно использовать мутабельный default как кэш или накопитель состояния внутри функции (намеренно). Но это трюк, который легко приводит к багам и снижает читаемость.
  • Опасность: большинство ожидает, что значение по умолчанию — "свежий" объект при каждом вызове. Когда это не так — возникают трудноуловимые ошибки, особенно в коде с тестами и многими вызовами.

Выводы

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

Такая простая деталь может привести к тонким багам; понимание семантики дефолтных аргументов — важная часть грамотного программирования на Python.

Report Page