Рассмотрите фрагмент на Python: def add_item(item, lst=[]): lst.append(item); return lst; — объясните поведение этого кода при последовательных вызовах, укажите потенциальные ошибки и предложите корректные альтернативы на нескольких уровнях (быстрое исправление, реструктуризация API)
В Python значение аргумента по умолчанию вычисляется один раз при определении функции. В вашем примере дефолтный список создаётся один раз и используется повторно при каждом вызове, когда параметр lst не передан.Следствие: последовательные вызовы накапливают элементы в одном и том же списке.
Пример поведения
def add_itemitem,lst=[]item, lst=[]item,lst=[]: lst.appenditemitemitem
return lst printadditem(1)add_item(1)additem(1) # 111
printadditem(2)add_item(2)additem(2) # 1,21, 21,2 <- неожиданный для многих результат printadditem(3)add_item(3)additem(3) # 1,2,31, 2, 31,2,3
Почему это плохо / потенциальные ошибки
Неочевидное накопление состояния — баги трудно отследить.Такое поведение распространяется на любые изменяемые дефолтные значения dict,set,объектыdict, set, объектыdict,set,объекты.Проблемы с многопоточностью: список общий между вызовами из разных потоков.Нарушение принципа минимальной неожиданности API — функция кажется чистой, но имеет сайд-эффекты.
Использовать None как дефолт и создавать новый список внутри: def add_itemitem,lst=Noneitem, lst=Noneitem,lst=None: if lst is None: lst =
lst.appenditemitemitem
return lst
Плюс: состояние стало явным, можно создавать несколько независимых коллекций.
4) Если нужен дефолт‑фабричный паттерн каквdataclassesкак в dataclassesкаквdataclasses:
Для параметров функции используют None-сентинел или фабрику, но в сигнатуре фабрика в явном виде: def add_itemitem,listfactory=list,lst=Noneitem, list_factory=list, lst=Noneitem,listfactory=list,lst=None: if lst is None: lst = list_factory
lst.appenditemitemitem
return lst
Это полезно, если нужен специфичный тип контейнера.
5) Если нужно защищать от непреднамеренной мутации переданного списка — работать с копией
def add_item_safeitem,lst=Noneitem, lst=Noneitem,lst=None: if lst is None: lst =
else: lst = listlstlstlst # копируем lst.appenditemitemitem
return lst
Рекомендация
Если вы не хотите сохранять состояние между вызовами — используйте паттерн с lst=None быстроеисправлениебыстрое исправлениебыстроеисправление или полностью неперезаписывающую возвращатьновыйсписоквозвращать новый списоквозвращатьновыйсписок.Если вам нужно именно разделяемое состояние — сделайте его явным классилизамыканиекласс или замыканиеклассилизамыкание и задокументируйте.
Дополнительные замечания
Этот трюк касается любых изменяемых дефолтных значений.Документируйте поведение функции, чтобы не вводить в заблуждение пользователей API.
Коротко — что происходит
В Python значение аргумента по умолчанию вычисляется один раз при определении функции. В вашем примере дефолтный список создаётся один раз и используется повторно при каждом вызове, когда параметр lst не передан.Следствие: последовательные вызовы накапливают элементы в одном и том же списке.Пример поведения
def add_itemitem,lst=[]item, lst=[]item,lst=[]:lst.appenditemitemitem return lst
printadditem(1)add_item(1)addi tem(1) # 111 printadditem(2)add_item(2)addi tem(2) # 1,21, 21,2 <- неожиданный для многих результат
printadditem(3)add_item(3)addi tem(3) # 1,2,31, 2, 31,2,3
Почему это плохо / потенциальные ошибки
Неочевидное накопление состояния — баги трудно отследить.Такое поведение распространяется на любые изменяемые дефолтные значения dict,set,объектыdict, set, объектыdict,set,объекты.Проблемы с многопоточностью: список общий между вызовами из разных потоков.Нарушение принципа минимальной неожиданности API — функция кажется чистой, но имеет сайд-эффекты.Быстрое исправление самыйраспространённыйпаттернсамый распространённый паттернсамыйраспространённыйпаттерн
Использовать None как дефолт и создавать новый список внутри:def add_itemitem,lst=Noneitem, lst=Noneitem,lst=None:
if lst is None:
lst = lst.appenditemitemitem return lst
Теперь:
printadditem(1)add_item(1)addi tem(1) -> 111 printadditem(2)add_item(2)addi tem(2) -> 222 # независимые вызовы
Альтернативы / реструктуризация API несколькоуровнейнесколько уровнейнесколькоуровней
1) Неп изменять входной список — чистая функция немутируетаргументыне мутирует аргументынемутируетаргументы
def add_itemitem,lst=Noneitem, lst=Noneitem,lst=None:if lst is None:
return itemitemitem return lst + itemitemitem # возвращает новый список
Плюсы: безопасно в многопоточных средах, легче тестировать.
2) Явно требовать список чёткопоказать,чтопроисходитмутациячётко показать, что происходит мутациячёткопоказать,чтопроисходитмутация
def append_toitem,lstitem, lstitem,lst:lst.appenditemitemitem return lst
Плюс: API явно мутабельный, нельзя по ошибке забыть передать.
3) Если нужно сохранять состояние между вызовами — сделать это явно класс/замыканиекласс/замыканиекласс/замыкание
С классом:class Collector:
def __init__selfselfself:
self.items = def addself,itemself, itemself,item:
self.items.appenditemitemitem return self.itemsС замыканием:
def make_collector:
items = def additemitemitem:
items.appenditemitemitem return items
return add
Плюс: состояние стало явным, можно создавать несколько независимых коллекций.
4) Если нужен дефолт‑фабричный паттерн каквdataclassesкак в dataclassesкаквdataclasses:
Для параметров функции используют None-сентинел или фабрику, но в сигнатуре фабрика в явном виде:def add_itemitem,listfactory=list,lst=Noneitem, list_factory=list, lst=Noneitem,listf actory=list,lst=None:
if lst is None:
lst = list_factory lst.appenditemitemitem return lst
Это полезно, если нужен специфичный тип контейнера.
5) Если нужно защищать от непреднамеренной мутации переданного списка — работать с копией
def add_item_safeitem,lst=Noneitem, lst=Noneitem,lst=None:if lst is None:
lst = else:
lst = listlstlstlst # копируем
lst.appenditemitemitem return lst
Рекомендация
Если вы не хотите сохранять состояние между вызовами — используйте паттерн с lst=None быстроеисправлениебыстрое исправлениебыстроеисправление или полностью неперезаписывающую возвращатьновыйсписоквозвращать новый списоквозвращатьновыйсписок.Если вам нужно именно разделяемое состояние — сделайте его явным классилизамыканиекласс или замыканиеклассилизамыкание и задокументируйте.Дополнительные замечания
Этот трюк касается любых изменяемых дефолтных значений.Документируйте поведение функции, чтобы не вводить в заблуждение пользователей API.