Приведён фрагмент на Python: def f(x=[]): x.append(1); return x; Объясните, почему вызовы f() возвращают растущий список, какие механизмы языка это порождают и как правильно реализовать ожидаемое поведение
Кратко: проблема в том, что значение аргумента по умолчанию вычисляется один раз при определении функции, и если это изменяемый объект (список), то все вызовы будут работать с одним и тем же объектом — его изменения (append) сохраняются. Почему так происходит (механизмы языка): - Значение дефолтного аргумента вычисляется при выполнении оператора def, не при каждом вызове. - Если дефолт — изменяемый объект (list, dict и т.п.), то функция хранит ссылку на этот конкретный объект и при вызове использует его. - Операция x.append изменяет объект «на месте», поэтому следующие вызовы увидят накопленные изменения. Демонстрация (поведение оригинального примера): def f(x=[]): print(id(x)) x.append(111) return x f() # тот же id, возвращает [111] f() # тот же id, возвращает [111, 111] ... Как правильно (обычное решение — использовать None как «маркер» и создавать новый список при необходимости): def f(x=None): if x is None: x = [] x.append(111) return x Альтернативы: - использовать уникальный маркер: _sentinel = object(); def f(x=_sentinel): if x is _sentinel: x = [] - принимать неизменяемый дефолт (например, tuple) или копировать дефолт внутри функции: x = list(x) при необходимости. Вывод: не используйте изменяемые объекты как значения по умолчанию; предпочтительный шаблон — дефолт None + создание нового объекта внутри функции.
Почему так происходит (механизмы языка):
- Значение дефолтного аргумента вычисляется при выполнении оператора def, не при каждом вызове.
- Если дефолт — изменяемый объект (list, dict и т.п.), то функция хранит ссылку на этот конкретный объект и при вызове использует его.
- Операция x.append изменяет объект «на месте», поэтому следующие вызовы увидят накопленные изменения.
Демонстрация (поведение оригинального примера):
def f(x=[]):
print(id(x))
x.append(111)
return x
f() # тот же id, возвращает [111]
f() # тот же id, возвращает [111, 111]
...
Как правильно (обычное решение — использовать None как «маркер» и создавать новый список при необходимости):
def f(x=None):
if x is None:
x = []
x.append(111)
return x
Альтернативы:
- использовать уникальный маркер: _sentinel = object(); def f(x=_sentinel): if x is _sentinel: x = []
- принимать неизменяемый дефолт (например, tuple) или копировать дефолт внутри функции: x = list(x) при необходимости.
Вывод: не используйте изменяемые объекты как значения по умолчанию; предпочтительный шаблон — дефолт None + создание нового объекта внутри функции.