Найдите и объясните проблему в этом Python‑кейсе: def add_item(item, lst=[]): lst.append(item) return lst Почему такое поведение встречает программистов и как его правильно протестировать и исправить
Проблема в том, что параметр по умолчанию lst=[] — это изменяемый объект, который создаётся один раз при определении функции и потом переиспользуется при каждом вызове без явного аргумента. В результате successive вызовы накапливают элементы в одном и том же списке, что часто неожиданно.
Почему так происходит
Значение аргумента по умолчанию вычисляется один раз при определении функции (time of function definition), а не при каждом вызове.Если это изменяемый объект (список, dict, set и т.п.), то изменения внутри функции сохраняются между вызовами.
print(add_item_bug(1)) # [1] print(add_item_bug(2)) # [1, 2] <- неожиданно для многих print(add_item_bug(3)) # [1, 2, 3]
Как правильно исправить Использовать None (или другой «сентинел») в качестве значения по умолчанию и создавать новый список внутри функции: def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst
Поведение после исправления: print(add_item(1)) # [1] print(add_item(2)) # [2] print(add_item(3)) # [3]
Как правильно протестировать 1) Тест на накапливание (покажет баг): def test_add_item_bug(): assert add_item_bug(1) == [1] assert add_item_bug(2) == [1, 2] # ожидаемо неверное поведение, тест демонстрирует баг
2) Тест для исправленной версии (практически): def test_add_item_fixed(): assert add_item(1) == [1] assert add_item(2) == [2] # теперь новый список при втором вызове
3) Дополнительно проверить передачу собственного списка: def test_add_item_with_explicit_list(): base = [10] res = add_item(1, base) assert res is base and res == [10, 1]
(эти тесты можно написать в unittest или pytest)
Дополнительные замечания
Иногда повторное использование мутируемого аргумента делается намеренно (например, кэширование). В этом случае такой код осознан и должен быть задокументирован.Для потокобезопасности общий список всё равно может приводить к гонкам — при параллельном доступе нужно синхронизировать или использовать другие структуры.Аналогичная проблема возникает с любыми изменяемыми значениями по умолчанию (dict, set, экземпляр класса и т.п.).
Краткий вывод Нельзя использовать изменяемые объекты как значения по умолчанию, если не хотите, чтобы они разделялись между вызовами. Используйте None и создавайте новый объект внутри функции.
Проблема в том, что параметр по умолчанию lst=[] — это изменяемый объект, который создаётся один раз при определении функции и потом переиспользуется при каждом вызове без явного аргумента. В результате successive вызовы накапливают элементы в одном и том же списке, что часто неожиданно.
Почему так происходит
Значение аргумента по умолчанию вычисляется один раз при определении функции (time of function definition), а не при каждом вызове.Если это изменяемый объект (список, dict, set и т.п.), то изменения внутри функции сохраняются между вызовами.Пример демонстрации (поведение «до» исправления):
def add_item_bug(item, lst=[]):
lst.append(item)
return lst
print(add_item_bug(1)) # [1]
print(add_item_bug(2)) # [1, 2] <- неожиданно для многих
print(add_item_bug(3)) # [1, 2, 3]
Как правильно исправить
Использовать None (или другой «сентинел») в качестве значения по умолчанию и создавать новый список внутри функции:
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Поведение после исправления:
print(add_item(1)) # [1]
print(add_item(2)) # [2]
print(add_item(3)) # [3]
Как правильно протестировать
1) Тест на накапливание (покажет баг):
def test_add_item_bug():
assert add_item_bug(1) == [1]
assert add_item_bug(2) == [1, 2] # ожидаемо неверное поведение, тест демонстрирует баг
2) Тест для исправленной версии (практически):
def test_add_item_fixed():
assert add_item(1) == [1]
assert add_item(2) == [2] # теперь новый список при втором вызове
3) Дополнительно проверить передачу собственного списка:
def test_add_item_with_explicit_list():
base = [10]
res = add_item(1, base)
assert res is base and res == [10, 1]
(эти тесты можно написать в unittest или pytest)
Дополнительные замечания
Иногда повторное использование мутируемого аргумента делается намеренно (например, кэширование). В этом случае такой код осознан и должен быть задокументирован.Для потокобезопасности общий список всё равно может приводить к гонкам — при параллельном доступе нужно синхронизировать или использовать другие структуры.Аналогичная проблема возникает с любыми изменяемыми значениями по умолчанию (dict, set, экземпляр класса и т.п.).Краткий вывод
Нельзя использовать изменяемые объекты как значения по умолчанию, если не хотите, чтобы они разделялись между вызовами. Используйте None и создавайте новый объект внутри функции.