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

R01 и R02 ниже обозначают вымышленные идентификаторы двух учебных записей. Никаких сведений о реальных участниках в примере нет. Полный код выполнен в Python 3.12.14 без внешних пакетов. Он показывает поведение списков и вызовов, а не работу реальной программы сбора данных или интерфейса приложения.

Когда появляется список по умолчанию

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

В нашем collect_bad метод append для изменяемой последовательности добавляет ID в уже существующий список. Возврат rows не делает снимок списка. Поэтому переменная first продолжает ссылаться на тот же объект и после второго вызова показывает обновлённое содержимое. Проверка is сравнивает тождество объектов; она не заменяет сравнение содержимого через ==.

Момент или вариант   Результат учебного примера
После первого collect_bad   first содержит R01
После второго collect_bad   second и first содержат R01, R02; это один объект
Два вызова collect без rows   Получены отдельные списки R01 и R02
Два вызова collect с одним bucket   Один явно переданный список намеренно содержит R01, R02
Авторская схема на учебных данных, не интерфейс. Один список между вызовами. Аргумент rows=[] После R01: [R01] После R02: [R01, R02]; first тоже изменился. Аргумент rows=None Новые списки: [R01] и [R02] Каждый вызов без списка создаёт свой список. Явный общий bucket После двух вызовов: [R01, R02] Накопление здесь задано намеренно. Учебные ID. Авторская схема, не интерфейс программы.
Авторская схема на учебных данных, не интерфейс. Один список между вызовами. Аргумент rows=[] После R01: [R01] После R02: [R01, R02]; first тоже изменился. Аргумент rows=None Новые списки: [R01] и [R02] Каждый вызов без списка создаёт свой список. Явный общий bucket После двух вызовов: [R01, R02] Накопление здесь задано намеренно. Учебные ID. Авторская схема, не интерфейс программы.

Законченный пример: ошибка, исправление и намеренное накопление

В выводе first_before показывает содержимое first до второго вызова, а first_after показывает содержимое после него. Переменная shared показывает, один ли это объект с second. Пара fresh содержит два результата исправленной функции, fresh_shared проверяет их тождество. Bucket обозначает явно переданный накопитель, а не скрытое значение по умолчанию.

Ниже приведён полный код учебного примера и фактически полученный вывод. Обозначения в выводе объяснены рядом с примером.

def collect_bad(case_id, rows=[]):
    rows.append(case_id)
    return rows

first = collect_bad("R01")
print("first_before =", first)
second = collect_bad("R02")
print("second =", second)
print("first_after =", first)
print("shared =", first is second)

def collect(case_id, rows=None):
    if rows is None:
        rows = []
    rows.append(case_id)
    return rows

a = collect("R01")
b = collect("R02")
print("fresh =", a, b)
print("fresh_shared =", a is b)
bucket = []
collect("R01", bucket)
collect("R02", bucket)
print("bucket =", bucket)
assert first is second and first == ["R01", "R02"]
assert a == ["R01"] and b == ["R02"] and a is not b
assert bucket == ["R01", "R02"]
first_before = ['R01']
second = ['R01', 'R02']
first_after = ['R01', 'R02']
shared = True
fresh = ['R01'] ['R02']
fresh_shared = False
bucket = ['R01', 'R02']

None в исправленной функции означает «создать новый список». Проверка rows is None важна: пустой список, который передал вызывающий код, остаётся тем самым списком. Подстановка rows or [] выражала бы другое правило и могла бы потерять намерение работать с переданным пустым накопителем. В этом примере контракт функции специально предполагает список или None, а не любой объект.

Как выбрать поведение для своей задачи

  • Если каждая порция должна иметь собственный результат, создавайте список внутри вызова при rows is None. Проверьте два последовательных вызова, а не только первый удачный пример.
  • Если требуется накопление, создайте bucket в вызывающем коде и передайте его явно. Зафиксируйте, когда его нужно очистить и где начинается новая порция.
  • Не воспринимайте присваивание first = result как копирование данных. Если необходим независимый снимок, отдельно выберите подходящий способ копирования для своей структуры.
  • Проверьте другие изменяемые значения по умолчанию: словари и пользовательские объекты также могут сохранять изменения. Не объявляйте любое значение по умолчанию ошибкой: важно, изменяется ли объект и соответствует ли это намерению.

Исправление rows=None создаёт новый внешний список только в указанном случае. Оно не копирует вложенные объекты, не изолирует произвольно переданные структуры и не делает весь процесс автоматически безопасным для параллельного выполнения. Границы примера нужно сохранять при переносе в реальный код: здесь работают короткие последовательные вызовы с двумя заранее заданными ID.

Что занести в журнал ошибки

Вопрос   Ответ в учебном случае
Что воспроизводит сбой   Два вызова collect_bad без второго аргумента
Что изменилось   Общий список получил R02; старая ссылка first тоже видит его
Как проверено исправление   a и b имеют разные ID и a is b даёт False
Что оставлено намеренным   Один явно переданный bucket накапливает оба ID

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

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