В представленном Python-фрагменте: def append_number(x, lst=[]): lst.append(x); return lst; объясните, почему этот код ведёт себя неожиданно при повторных вызовах, как это связано с семантикой объектов и областей видимости, и предложите корректные альтернативы
Проблема и причина (кратко) - При выполнении определения функции значение аргумента по умолчанию вычисляется один раз и привязывается к имени параметра. В вашем примере при определении создаётся один объект списка и затем переиспользуется при каждом вызове функции. Поскольку списки изменяемы, мутации в одном вызове сохраняются для последующих. - Следствие: первый вызов `append_number(1)` вернёт [1][1][1], второй `append_number(2)` вернёт [1,2][1, 2][1,2] — не новый список, а тот же объект с накопленными значениями. Как это связано с семантикой объектов и областей видимости - Значение default-аргумента вычисляется в момент определения функции (не при вызове). Параметр ссылается на один и тот же объект (идентичность), а не на «новое значение» при каждом вызове. - Это поведение связано с семантикой ссылочных типов в Python: переменные хранят ссылки на объекты; если объект изменяем — изменения видны всем ссылкам на него. Корректные альтернативы 1) Простой идиоматический вариант — использовать `None` как sentinel и создавать новый список при вызове: def append_number(x, lst=None): if lst is None: lst = [] lst.append(x) return lst Пример: append_number(1) → [1][1][1], следующий append_number(2) → [2][2][2]. 2) Не мутировать переданный список, а возвращать новый: def append_number(x, lst=None): if lst is None: return [x] return lst + [x] Это безопасно, т.к. создаётся новый список при каждом вызове. 3) Если функция должна принимать существующий список, но не менять его — сделать копию перед изменением: def append_number(x, lst=None): if lst is None: lst = [] else: lst = list(lst) # копия lst.append(x) return lst 4) Если вам нужен разделяемый по умолчанию единый контейнер — делайте это явно и задокументируйте (но это редко желаемо): _default = [] def append_number(x, lst=_default): lst.append(x) return lst Замечание: будьте осторожны с побочными эффектами и многопоточностью; предпочтительны варианты (1) или (2). Короткая суть: не используйте изменяемый объект как значение аргумента по умолчанию, если вы ожидаете «пустой контейнер» при каждом вызове — используйте `None` + создание нового контейнера.
- При выполнении определения функции значение аргумента по умолчанию вычисляется один раз и привязывается к имени параметра. В вашем примере при определении создаётся один объект списка и затем переиспользуется при каждом вызове функции. Поскольку списки изменяемы, мутации в одном вызове сохраняются для последующих.
- Следствие: первый вызов `append_number(1)` вернёт [1][1][1], второй `append_number(2)` вернёт [1,2][1, 2][1,2] — не новый список, а тот же объект с накопленными значениями.
Как это связано с семантикой объектов и областей видимости
- Значение default-аргумента вычисляется в момент определения функции (не при вызове). Параметр ссылается на один и тот же объект (идентичность), а не на «новое значение» при каждом вызове.
- Это поведение связано с семантикой ссылочных типов в Python: переменные хранят ссылки на объекты; если объект изменяем — изменения видны всем ссылкам на него.
Корректные альтернативы
1) Простой идиоматический вариант — использовать `None` как sentinel и создавать новый список при вызове:
def append_number(x, lst=None):
if lst is None:
lst = []
lst.append(x)
return lst
Пример: append_number(1) → [1][1][1], следующий append_number(2) → [2][2][2].
2) Не мутировать переданный список, а возвращать новый:
def append_number(x, lst=None):
if lst is None:
return [x]
return lst + [x]
Это безопасно, т.к. создаётся новый список при каждом вызове.
3) Если функция должна принимать существующий список, но не менять его — сделать копию перед изменением:
def append_number(x, lst=None):
if lst is None:
lst = []
else:
lst = list(lst) # копия
lst.append(x)
return lst
4) Если вам нужен разделяемый по умолчанию единый контейнер — делайте это явно и задокументируйте (но это редко желаемо):
_default = []
def append_number(x, lst=_default):
lst.append(x)
return lst
Замечание: будьте осторожны с побочными эффектами и многопоточностью; предпочтительны варианты (1) или (2).
Короткая суть: не используйте изменяемый объект как значение аргумента по умолчанию, если вы ожидаете «пустой контейнер» при каждом вызове — используйте `None` + создание нового контейнера.