Рассмотрите фрагмент на Python: def add_item(item, lst=[]): lst.append(item); return lst; — объясните поведение этого кода при последовательных вызовах, укажите потенциальные ошибки и предложите корректные альтернативы на нескольких уровнях (быстрое исправление, реструктуризация API)

12 Окт 2025 в 14:18
22 +1
0
Ответы
1

Коротко — что происходит

В Python значение аргумента по умолчанию вычисляется один раз при определении функции. В вашем примере дефолтный список [] создаётся один раз и используется повторно при каждом вызове, когда параметр lst не передан.Следствие: последовательные вызовы накапливают элементы в одном и том же списке.

Пример поведения

def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] <- неожиданный для многих результат
print(add_item(3)) # [1, 2, 3]

Почему это плохо / потенциальные ошибки

Неочевидное накопление состояния — баги трудно отследить.Такое поведение распространяется на любые изменяемые дефолтные значения (dict, set, объекты).Проблемы с многопоточностью: список общий между вызовами из разных потоков.Нарушение принципа минимальной неожиданности API — функция кажется чистой, но имеет сайд-эффекты.

Быстрое исправление (самый распространённый паттерн)

Использовать 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] # независимые вызовы

Альтернативы / реструктуризация API (несколько уровней)

1) Неп изменять входной список — чистая функция (не мутирует аргументы)

def add_item(item, lst=None):
if lst is None:
return [item]
return lst + [item] # возвращает новый список

Плюсы: безопасно в многопоточных средах, легче тестировать.

2) Явно требовать список (чётко показать, что происходит мутация)

def append_to(item, lst):
lst.append(item)
return lst

Плюс: API явно мутабельный, нельзя по ошибке забыть передать.

3) Если нужно сохранять состояние между вызовами — сделать это явно (класс/замыкание)

С классом:
class Collector:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
return self.itemsС замыканием:
def make_collector():
items = []
def add(item):
items.append(item)
return items
return add

Плюс: состояние стало явным, можно создавать несколько независимых коллекций.

4) Если нужен дефолт‑фабричный паттерн (как в dataclasses):

Для параметров функции используют None-сентинел или фабрику, но в сигнатуре фабрика в явном виде:
def add_item(item, list_factory=list, lst=None):
if lst is None:
lst = list_factory()
lst.append(item)
return lst

Это полезно, если нужен специфичный тип контейнера.

5) Если нужно защищать от непреднамеренной мутации переданного списка — работать с копией

def add_item_safe(item, lst=None):
if lst is None:
lst = []
else:
lst = list(lst) # копируем
lst.append(item)
return lst

Рекомендация

Если вы не хотите сохранять состояние между вызовами — используйте паттерн с lst=None (быстрое исправление) или полностью неперезаписывающую (возвращать новый список).Если вам нужно именно разделяемое состояние — сделайте его явным (класс или замыкание) и задокументируйте.

Дополнительные замечания

Этот трюк касается любых изменяемых дефолтных значений.Документируйте поведение функции, чтобы не вводить в заблуждение пользователей API.
12 Окт 2025 в 15:30
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир