Дан фрагмент Python: "def foo(x=[]): x.append(1); return x" — объясните поведение функции при последовательных вызовах, причины этого поведения и способы избежать неожиданных эффектов
Поведение - При последовательных вызовах функция будет накапливать элементы в одном и том же списке, т.е.: - первый вызов: результат \([\(1\)]\), - второй: результат \([\(1\), \(1\)]\), - третий: результат \([\(1\), \(1\), \(1\)]\). - Причина: объект по умолчанию для аргумента xxx (список) создаётся один раз при определении функции, а не при каждом вызове; все вызовы используют одну и ту же ссылку (см. `foo.__defaults__`). Пример (поведение): def foo(x=[]): x.append(111) return x print(foo()) # -> [111] print(foo()) # -> [111, 111] print(foo()) # -> [111, 111, 111] Как избежать неожиданных эффектов 1. Использовать «маркер» `None` и создавать новый список внутри: def foo(x=None): if x is None: x = [] x.append(111) return x 2. Использовать уникальный маркер: _sentinel = object() def foo(x=_sentinel): if x is _sentinel: x = [] x.append(111) return x 3. Не менять значение по умолчанию, а работать с копией: def foo(x=None): x = list(x) if x is not None else [] x.append(111) return x 4. В специфичных случаях (когда нужно разделяемое состояние) — намеренно использовать мутируемый объект по умолчанию, но делать это осознанно и документировать. Кратко: проблема в том, что дефолтные значения вычисляются один раз при определении; безопасный и общий способ — использовать `None` и создавать новый список внутри функции.
- При последовательных вызовах функция будет накапливать элементы в одном и том же списке, т.е.:
- первый вызов: результат \([\(1\)]\),
- второй: результат \([\(1\), \(1\)]\),
- третий: результат \([\(1\), \(1\), \(1\)]\).
- Причина: объект по умолчанию для аргумента xxx (список) создаётся один раз при определении функции, а не при каждом вызове; все вызовы используют одну и ту же ссылку (см. `foo.__defaults__`).
Пример (поведение):
def foo(x=[]):
x.append(111)
return x
print(foo()) # -> [111]
print(foo()) # -> [111, 111]
print(foo()) # -> [111, 111, 111]
Как избежать неожиданных эффектов
1. Использовать «маркер» `None` и создавать новый список внутри:
def foo(x=None):
if x is None:
x = []
x.append(111)
return x
2. Использовать уникальный маркер:
_sentinel = object()
def foo(x=_sentinel):
if x is _sentinel:
x = []
x.append(111)
return x
3. Не менять значение по умолчанию, а работать с копией:
def foo(x=None):
x = list(x) if x is not None else []
x.append(111)
return x
4. В специфичных случаях (когда нужно разделяемое состояние) — намеренно использовать мутируемый объект по умолчанию, но делать это осознанно и документировать.
Кратко: проблема в том, что дефолтные значения вычисляются один раз при определении; безопасный и общий способ — использовать `None` и создавать новый список внутри функции.