Кейс на Python: def add_item(item, lst=[]): lst.append(item) ; return lst Объясните, почему использование изменяемого аргумента по умолчанию опасно, какие баги вызывает этот код и какие корректные альтернативы предложить
Проблема: значение аргумента по умолчанию вычисляется один раз при определении функции, а не при каждом вызове. Для изменяемого объекта (например, списка) это значит, что все вызовы используют один и тот же объект — он накапливает состояние между вызовами. Пример бага (поведение, которое часто неожиданно): - при вызове add_item(111) функция вернёт [1][1][1]; - при последующем вызове add_item(222) вернётся уже [1,2][1, 2][1,2] — список не «сбросился». Баги и последствия - Непредсказуемое накопление данных между вызовами. - Трудноотлавливаемые состояния в больших кодовых базах. - Проблемы безопасности/утечка памяти при долгоживущих процессах. - Потенциальные гонки при многопоточном использовании (разделяемый объект). Правильные альтернативы 1) Стандартный паттерн — sentinel None: def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst 2) Не мутировать входной аргумент — возвращать новый объект: def add_item(item, lst=None): if lst is None: lst = [] return lst + [item] 3) Использовать неизменяемый default и конвертацию: def add_item(item, lst=()): return list(lst) + [item] 4) Если вам нужно намеренно хранить состояние между вызовами — делайте это явно (класс или замыкание), а не через «скрытый» default: class Accumulator: def __init__(self): self.lst = [] def add(self, item): self.lst.append(item) return self.lst Рекомендация: по умолчанию используйте None для изменяемых аргументов, чтобы при каждом вызове получать новый объект, если только явная общая память не требуется.
Пример бага (поведение, которое часто неожиданно):
- при вызове add_item(111) функция вернёт [1][1][1];
- при последующем вызове add_item(222) вернётся уже [1,2][1, 2][1,2] — список не «сбросился».
Баги и последствия
- Непредсказуемое накопление данных между вызовами.
- Трудноотлавливаемые состояния в больших кодовых базах.
- Проблемы безопасности/утечка памяти при долгоживущих процессах.
- Потенциальные гонки при многопоточном использовании (разделяемый объект).
Правильные альтернативы
1) Стандартный паттерн — sentinel None:
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
2) Не мутировать входной аргумент — возвращать новый объект:
def add_item(item, lst=None):
if lst is None:
lst = []
return lst + [item]
3) Использовать неизменяемый default и конвертацию:
def add_item(item, lst=()):
return list(lst) + [item]
4) Если вам нужно намеренно хранить состояние между вызовами — делайте это явно (класс или замыкание), а не через «скрытый» default:
class Accumulator:
def __init__(self):
self.lst = []
def add(self, item):
self.lst.append(item)
return self.lst
Рекомендация: по умолчанию используйте None для изменяемых аргументов, чтобы при каждом вызове получать новый объект, если только явная общая память не требуется.