Рассмотрите следующий фрагмент на Python: def append_unique(item, lst=[]): if item not in lst: lst.append(item) return lst Объясните, почему поведение этой функции может быть неожиданным при многократных вызовах, какие проблемы это вызывает, и предложите корректные альтернативы для разного уровня требований (простое исправление, идиоматическое решение, безопасный вариант для многопоточной среды)
def append_unique(item, lst=[]): if item not in lst: lst.append(item) return lst
работает не так, как многие ожидают, потому что значение по умолчанию lst=[] создаётся один раз — при определении функции — и потом переиспользуется при всех последующих вызовах, где аргумент lst не передан. В результате список «накапливает» значения между вызовами:
append_unique(1) [1] append_unique(2) [1, 2] append_unique(1) [1, 2] # 1 уже есть append_unique(3) [1, 2, 3]
Ожидаемое поведение (часто) — получить новый пустой список при каждом вызове без аргумента. Непредсказуемое совместное состояние приводит к багам, трудноотлавливаемым ошибкам и уязвимостям (особенно если список используется для накопления данных).
Какие проблемы вызывает
«Паразитное» состояние между вызовами (side effects) — функция не чистая.Непредсказуемость и трудности в отладке.Если функция документирована как создающая новый список при отсутствии аргумента, это ломает API.В многопоточной среде совместный список может стать точкой гонки; нарушение инвариантов и исключения.
Правильные альтернативы (несколько вариантов для разных требований)
1) Простое исправление (общепринятое) Использовать None как значение по умолчанию и создавать новый список внутри:
def append_unique(item, lst=None): if lst is None: lst = [] if item not in lst: lst.append(item) return lst
Это устраняет повторное использование одного и того же объекта по умолчанию. Это самый простой и идиоматичный способ избавиться от «mutable default» бага.
2) Идиоматическое/без мутации входного аргумента Если вы хотите, чтобы функция не изменила переданный список (т.е. всегда возвращала новый список), скопируйте вход:
def append_unique(item, lst=None): base = list(lst) if lst is not None else [] if item not in base: base.append(item) return base
Это полезно, когда ожидается неизменяемость внешнего состояния (функции без сайд-эффектов).
3) Эффективно для больших данных (быстрая проверка вхождения) Проверка item in lst — O(n). Если производительность важна и у вас много элементов, используйте set для теста уникальности (и, при необходимости, отдельный список для порядка):
class OrderedUnique: def init(self): self.lst = [] self.seen = set() def add(self, item): if item not in self.seen: self.seen.add(item) self.lst.append(item) def as_list(self): return list(self.lst)
4) Потокобезопасный вариант Если список должен разделяться между потоками, обеспечьте синхронизацию (Lock) или избегайте общей мутации (copy-on-write):
import threading
class ThreadSafeUnique: def init(self): self.lst = [] self.lock = threading.Lock() def append_unique(self, item): with self.lock: if item not in self.lst: self.lst.append(item)
возвращаем копию, чтобы внешнему коду не давать прямой доступ return list(self.lst)
Альтернатива без общей мутации: возвращать новый список при каждом вызове (copy-on-write) — тогда mutex не нужен, но затраты на копирование выше.
5) Если вы хотите намеренно иметь «сохранённое» состояние между вызовами Тогда делайте это явным: храните список в глобальной переменной или в атрибуте функции/классе и документируйте поведение. Такой код должен быть очевиден для читателя и тестов.
Рекомендация
Самый частый и правильный выбор: использовать lst=None и внутри создавать список (вариант 1). Это решает проблему и понятен любому Python-разработчику.Если функция не должна мутировать аргумент — копируйте вход (вариант 2).Для многопоточности — использовать блокировку или специально разработанную потокобезопасную структуру (вариант 4).
Если нужно, могу привести конкретный пример для вашего случая (например, если важен порядок элементов или нужно многопоточное добавление с сохранением порядка).
Код, который вы привели,
def append_unique(item, lst=[]):
if item not in lst:
lst.append(item)
return lst
работает не так, как многие ожидают, потому что значение по умолчанию lst=[] создаётся один раз — при определении функции — и потом переиспользуется при всех последующих вызовах, где аргумент lst не передан. В результате список «накапливает» значения между вызовами:
append_unique(1)
[1]
append_unique(2)
[1, 2]
append_unique(1)
[1, 2] # 1 уже есть
append_unique(3)
[1, 2, 3]
Ожидаемое поведение (часто) — получить новый пустой список при каждом вызове без аргумента. Непредсказуемое совместное состояние приводит к багам, трудноотлавливаемым ошибкам и уязвимостям (особенно если список используется для накопления данных).
Какие проблемы вызывает
«Паразитное» состояние между вызовами (side effects) — функция не чистая.Непредсказуемость и трудности в отладке.Если функция документирована как создающая новый список при отсутствии аргумента, это ломает API.В многопоточной среде совместный список может стать точкой гонки; нарушение инвариантов и исключения.Правильные альтернативы (несколько вариантов для разных требований)
1) Простое исправление (общепринятое)
Использовать None как значение по умолчанию и создавать новый список внутри:
def append_unique(item, lst=None):
if lst is None:
lst = []
if item not in lst:
lst.append(item)
return lst
Это устраняет повторное использование одного и того же объекта по умолчанию. Это самый простой и идиоматичный способ избавиться от «mutable default» бага.
2) Идиоматическое/без мутации входного аргумента
Если вы хотите, чтобы функция не изменила переданный список (т.е. всегда возвращала новый список), скопируйте вход:
def append_unique(item, lst=None):
base = list(lst) if lst is not None else []
if item not in base:
base.append(item)
return base
Это полезно, когда ожидается неизменяемость внешнего состояния (функции без сайд-эффектов).
3) Эффективно для больших данных (быстрая проверка вхождения)
Проверка item in lst — O(n). Если производительность важна и у вас много элементов, используйте set для теста уникальности (и, при необходимости, отдельный список для порядка):
class OrderedUnique:
def init(self):
self.lst = []
self.seen = set()
def add(self, item):
if item not in self.seen:
self.seen.add(item)
self.lst.append(item)
def as_list(self):
return list(self.lst)
4) Потокобезопасный вариант
Если список должен разделяться между потоками, обеспечьте синхронизацию (Lock) или избегайте общей мутации (copy-on-write):
import threading
class ThreadSafeUnique:
возвращаем копию, чтобы внешнему коду не давать прямой доступ return list(self.lst)def init(self):
self.lst = []
self.lock = threading.Lock()
def append_unique(self, item):
with self.lock:
if item not in self.lst:
self.lst.append(item)
Альтернатива без общей мутации: возвращать новый список при каждом вызове (copy-on-write) — тогда mutex не нужен, но затраты на копирование выше.
5) Если вы хотите намеренно иметь «сохранённое» состояние между вызовами
Тогда делайте это явным: храните список в глобальной переменной или в атрибуте функции/классе и документируйте поведение. Такой код должен быть очевиден для читателя и тестов.
Рекомендация
Самый частый и правильный выбор: использовать lst=None и внутри создавать список (вариант 1). Это решает проблему и понятен любому Python-разработчику.Если функция не должна мутировать аргумент — копируйте вход (вариант 2).Для многопоточности — использовать блокировку или специально разработанную потокобезопасную структуру (вариант 4).Если нужно, могу привести конкретный пример для вашего случая (например, если важен порядок элементов или нужно многопоточное добавление с сохранением порядка).