Что произойдёт с изменяемым аргументом по умолчанию функции при повторных вызовах и чем это объясняется?

Что произойдёт с изменяемым аргументом по умолчанию функции при повторных вызовах и чем это объясняется?

Проходите собеседования с ИИ помощником Hintsage

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

Изменяемый аргумент по умолчанию сохраняет изменения между вызовами функции. Это происходит потому, что значение по умолчанию создаётся один раз при определении функции, а не заново при каждом вызове.

Исторический контекст

Значения аргументов по умолчанию нужны для задания необязательных параметров и удобного описания стандартного поведения функции. В Python они хранятся вместе с объектом функции, поэтому не требуют повторного создания при каждом вызове.

Такой подход эффективен для неизменяемых объектов и позволяет использовать заранее подготовленные значения. Однако для изменяемых объектов он может привести к неожиданному сохранению состояния.

Постановка проблемы

Если значением по умолчанию является список, словарь или другой изменяемый объект, его изменение внутри функции будет видно при следующих вызовах. В результате данные одного вызова могут попасть в результат другого.

Особенно опасно это в обработчиках запросов, функциях преобразования данных и других местах, где разработчик ожидает независимость вызовов. Ошибка может проявляться только после нескольких обращений и выглядеть как случайное накопление состояния.

Подробное решение

При создании функции Python вычисляет выражение значения по умолчанию и сохраняет полученный объект. При вызове без соответствующего аргумента функция получает ссылку на тот же объект.

def collect(value, items=[]): items.append(value) return items print(collect(1)) # [1] print(collect(2)) # [1, 2]

В примере список создаётся один раз. Второй вызов получает тот же список, уже содержащий первый элемент.

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

def collect(value, items=None): if items is None: items = [] items.append(value) return items

Теперь каждый вызов без явного items получает новый список. При этом явно переданный список всё ещё изменяется, что следует учитывать в контракте функции.

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

Ситуация из практики

В функции подготовки отчёта список ошибок был задан как значение по умолчанию. После обработки нескольких файлов отчёт для нового файла включал ошибки из предыдущих файлов.

Рассматривались три варианта. Глобальный список был простым, но увеличивал область видимости и усложнял тестирование. Намеренное использование аргумента по умолчанию сохраняло состояние компактно, но делало его скрытым и небезопасным при параллельной обработке. Создание списка внутри функции через None устраняло неявное состояние.

Был выбран третий вариант: каждый отчёт должен быть независимым, а состояние передавалось явно при необходимости. В результате исчезло накопление ошибок между вызовами, а тесты перестали зависеть от порядка запуска.

Что кандидаты часто упускают

  1. Когда именно вычисляется значение аргумента по умолчанию?

Оно вычисляется при выполнении определения функции. Поэтому выражение для значения по умолчанию не пересчитывается при каждом вызове, а его результат сохраняется в объекте функции.

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

  1. Будет ли проблема, если функция вызывается с явно переданным списком?

Нет, в этом случае значение по умолчанию не используется. Функция получает переданный список, но если изменяет его содержимое, изменения будут видны вызывающему коду, потому что список передаётся как ссылка на тот же объект.

Таким образом, None защищает только от общего объекта по умолчанию. Он не запрещает функции изменять объект, который вызывающий код передал явно.

  1. Можно ли намеренно использовать изменяемый аргумент по умолчанию?

Да, если функция должна иметь постоянное внутреннее состояние между вызовами, например простой накопитель. Но это состояние нужно явно описать, поскольку оно влияет на порядок вызовов и может быть неожиданным для пользователей функции.

Для многопоточного или асинхронного кода такой подход требует дополнительной синхронизации либо отказа от общего изменяемого состояния. Если состояние не является частью осознанного контракта, безопаснее создавать объект внутри функции или передавать его явно.