Функция Python помнит предыдущий вызов: как исправить изменяемый аргумент по умолчанию

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

Обновлено 14 мин чтенияУчёба и навыки
Работа за ноутбуком с текстом в терминале

Первый вызов правильный, второй добавляет чужое значение

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

Разберём условный пример студентки Веры. Она пишет небольшую функцию add_comment: получает текст замечания, добавляет его в список и возвращает список. В определении второй аргумент comments имеет значение по умолчанию []. Вера рассчитывает, что отсутствие второго аргумента означает новую пустую подборку при каждом обращении. Но два последовательных вызова без этого аргумента работают с одним списком. Цель разбора — исправить именно границу состояния между вызовами и проверить договорённость о поведении функции.

Сначала сформулируйте ожидаемый результат

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

Есть и другая допустимая задача: функция специально дополняет общий переданный журнал. Тогда последовательное накопление ожидаемо. Прежде чем менять реализацию, определите требование задания. Для Веры отсутствие списка означает самостоятельный результат, а явно переданный журнал предполагает его дополнение. Эти два режима должны различаться понятно. Ошибка возникает не потому, что накопление всегда запрещено, а потому, что оно появляется без выраженного намерения вызывающего кода и нарушает представление о независимых работах.

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

Почему пустой список не появляется заново

Официальное руководство Python объясняет: значения аргументов по умолчанию вычисляются при определении функции, а не при каждом вызове. Список можно изменять. Поэтому операция append меняет тот объект, который уже связан со значением по умолчанию. Следующий вызов без второго аргумента получает доступ к его нынешнему содержимому. Значение выглядело пустым в тексте программы, но после первого добавления сам объект уже не пустой.

Представьте одну коробку, приготовленную в момент объявления функции. Отсутствие явной коробки означает использование приготовленной. После первого вызова внутри лежит карточка. Второй вызов кладёт следующую карточку туда же. Надпись «изначально пустая» на схеме не очищает коробку между действиями. Такая картинка помогает отличить время создания объекта от времени обращения к нему. В рабочем объяснении достаточно назвать оба момента и показать два последовательных результата.

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

Воспроизведите ошибку без большого проекта

Оставьте определение функции, добавление одного замечания и два вызова без второго аргумента. Уберите чтение файлов, интерфейс, случайные данные и остальные функции, если они не нужны для воспроизведения. На первом шаге сохраните результат первого вызова под именем first_comments. На втором получите second_comments. Затем выведите оба сохранённых результата. В ошибочной реализации после второго вызова обе переменные покажут два замечания: они связаны с одним изменённым списком.

Полезно проверить не только равенство содержимого, но и принадлежность одному объекту. В Python выражение first_comments is second_comments отвечает именно на вопрос об идентичности. Для двух независимых результатов ожидается False. Равенство через == проверяет другое: два разных пустых списка могут быть равны по содержимому. В нашем случае обнаружение True для is помогает локализовать общий объект, но само по себе ещё не определяет правильность: для намеренно общего журнала такой результат мог бы соответствовать договорённости.

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

Создавайте список внутри конкретного вызова

Для выбранного поведения удобно использовать None как признак отсутствующего списка. В определении comments получает значение None. В начале тела функция явно проверяет comments is None и только в этом случае создаёт новый пустой список. После этого добавляет замечание и возвращает результат. Новый список появляется во время соответствующего вызова. Следующий вызов без аргумента проходит ту же ветку и создаёт другой объект, не связанный с предыдущей подборкой.

Не заменяйте проверку на условие «если список пустой». Пустой список, переданный вызывающей стороной, может быть вполне осмысленным общим журналом. Проверка на ложность смешает два разных случая: аргумент отсутствует и аргумент передан, но пока не содержит записей. Для Веры это нарушит второй режим. Явное сравнение с None сохраняет различие и делает договорённость видимой начинающему читателю. Дополнительная универсальная обёртка или собственный класс для такого учебного примера не нужны.

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

Явно переданный список требует отдельного решения

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

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

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

Почему присваивание и очистка не решают задачу

Присваивание другого имени тому же списку не создаёт независимый список. Это подчёркивает документация модуля copy: присваивание связывает имя с объектом. Если сохранить общий журнал под новым именем и затем очистить его через clear, изменится тот же объект, доступный по старому имени. Иногда такой приём маскирует накопление: второй вызов показывает одну запись. Но первый сохранённый результат тоже внезапно изменится, и требование независимости окажется нарушено другим способом.

Очистка общего значения перед каждым вызовом также опасна по смыслу. Она уничтожает данные, на которые может ссылаться уже возвращённая подборка. В примере первый результат после второго вызова будет выглядеть как подборка второй работы. Это хуже заметно, если печатать результат только сразу после получения и не возвращаться к нему позже. Поэтому проверка сохранённого первого списка обязательна: она отличает настоящее разделение объектов от косметического удаления старого содержимого общей коробки.

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

Копия верхнего уровня не изолирует вложенные данные

Плоский список строк в нашем примере сравнительно прост. Если элементы сами являются списками или словарями, обычная поверхностная копия создаёт отдельную внешнюю коллекцию, но оставляет связи с вложенными объектами. Допустим, подборка содержит словарь с текстом и признаком проверки. Две внешние копии могут продолжать видеть один словарь. Изменение его признака через одну подборку станет видимым через другую. Это уже вопрос глубины копирования, который нужно отличать от исходного общего аргумента.

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

Проверьте, не создаёт ли общий объект вызывающий код

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

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

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

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

Проверьте порядок вызовов и исправьте объяснение

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

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

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

Была полезна статья?

Читайте также

Пригодится для подготовки