Рассмотрите фрагмент на Python: def add_item(item, lst=[]): lst.append(item); return lst; — объясните поведение этого кода при последовательных вызовах, укажите потенциальные ошибки и предложите корректные альтернативы на нескольких уровнях (быстрое исправление, реструктуризация API)
В 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 — функция кажется чистой, но имеет сайд-эффекты.
Плюс: состояние стало явным, можно создавать несколько независимых коллекций.
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.
Коротко — что происходит
В 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.