Разберите следующий фрагмент на Python: "def append_item(x, lst=[]): lst.append(x); return lst" — почему поведение функции может быть неожиданным при нескольких вызовах, как это исправить и какие ещё тонкости с изменяемыми аргументами существуют
Коротко и по делу. Почему поведение неожиданное - В Python значение аргумента по умолчанию вычисляется один раз при определении функции, а не при каждом вызове. Для функции def append_item(x, lst=[]): lst.append(x) return lst один и тот же список `lst` используется при всех вызывах. Пример: - append_item(1) возвращает [1][1][1]
- append_item(2) возвращает [1,2][1, 2][1,2]
— т.е. элементы накапливаются в одном и том же списке. Как исправить - Использовать "sentinel" `None` и создавать новый список внутри функции: def append_item(x, lst=None): if lst is None: lst = [] lst.append(x) return lst Теперь: - append_item(1) → [1][1][1]
- append_item(2) → [2][2][2] Другие тонкости с изменяемыми аргументами - По умолчанию безопасны неизменяемые типы (числа, строки, кортежи). Изменяемые (list, dict, set, пользовательские объекты) могут вести себя как общий состояние между вызовами. - Если функция должна не менять переданный аргумент, а вернуть новую коллекцию, копируйте: `lst = lst.copy()` или `new = list(old)`. Для вложенных структур — `copy.deepcopy`. - В классах и dataclass лучше использовать `default_factory` для полей-коллекций: from dataclasses import dataclass, field @dataclass class C: items: list = field(default_factory=list) - Замечание о замыкании: mutable в замыкании тоже хранится один объект; иногда используют аргумент по умолчанию, чтобы зафиксировать текущее значение. - Потокобезопасность: общий изменяемый объект между вызовами может привести к гонкам в многопоточной среде. - Шалловая копия vs глубокая: `copy()` не защищает от изменений вложенных изменяемых элементов. Резюме: не используйте изменяемые объекты как значения по умолчанию; применяйте `None` + создание нового объекта или специальные механизмы (default_factory, копирование) в зависимости от задачи.
Почему поведение неожиданное
- В Python значение аргумента по умолчанию вычисляется один раз при определении функции, а не при каждом вызове. Для функции
def append_item(x, lst=[]):
lst.append(x)
return lst
один и тот же список `lst` используется при всех вызывах. Пример:
- append_item(1) возвращает [1][1][1] - append_item(2) возвращает [1,2][1, 2][1,2] — т.е. элементы накапливаются в одном и том же списке.
Как исправить
- Использовать "sentinel" `None` и создавать новый список внутри функции:
def append_item(x, lst=None):
if lst is None:
lst = []
lst.append(x)
return lst
Теперь:
- append_item(1) → [1][1][1] - append_item(2) → [2][2][2]
Другие тонкости с изменяемыми аргументами
- По умолчанию безопасны неизменяемые типы (числа, строки, кортежи). Изменяемые (list, dict, set, пользовательские объекты) могут вести себя как общий состояние между вызовами.
- Если функция должна не менять переданный аргумент, а вернуть новую коллекцию, копируйте: `lst = lst.copy()` или `new = list(old)`. Для вложенных структур — `copy.deepcopy`.
- В классах и dataclass лучше использовать `default_factory` для полей-коллекций:
from dataclasses import dataclass, field
@dataclass
class C:
items: list = field(default_factory=list)
- Замечание о замыкании: mutable в замыкании тоже хранится один объект; иногда используют аргумент по умолчанию, чтобы зафиксировать текущее значение.
- Потокобезопасность: общий изменяемый объект между вызовами может привести к гонкам в многопоточной среде.
- Шалловая копия vs глубокая: `copy()` не защищает от изменений вложенных изменяемых элементов.
Резюме: не используйте изменяемые объекты как значения по умолчанию; применяйте `None` + создание нового объекта или специальные механизмы (default_factory, копирование) в зависимости от задачи.