Ловушка изменяемого аргумента по умолчанию
Любимый «вопрос с подвохом»: почему список-дефолт ведёт себя странно между вызовами.
Значение по умолчанию вычисляется один раз — в момент определения функции, а не при каждом вызове. Для изменяемого дефолта это значит общий объект на все вызовы.
Вопрос: что не так с target=[]?
Чёткий ответ. Список из def add(item, target=[]) создаётся единожды и переиспользуется при каждом вызове без аргумента. Поэтому он «копит» значения между вызовами — почти всегда это баг.
def add(item, target=[]):
target.append(item)
return target
print(add(1))
print(add(2)) # ожидали [2], а получаем [1, 2]
print(add(3)) # список тот же самый
Вывод:
[1] [1, 2] [1, 2, 3]
Каждый вызов без target работает с одним и тем же списком, который был создан в момент определения add. Значения накапливаются.
Правильное решение: None как сигнал
Стандартный приём — дефолт None, а внутри создавать новый объект при каждом вызове.
def add(item, target=None):
if target is None:
target = [] # новый список на каждый вызов
target.append(item)
return target
print(add(1))
print(add(2)) # теперь независимо
print(add(3))
Вывод:
[1] [2] [3]
Когда «один раз» — это полезно
То же свойство иногда применяют намеренно — например, для дешёвого кеша. Но делать это надо осознанно и с комментарием, иначе следующий разработчик примет это за ошибку.
def fib(n, _cache={0: 0, 1: 1}): # кеш живёт между вызовами намеренно
if n not in _cache:
_cache[n] = fib(n - 1) + fib(n - 2)
return _cache[n]
print(fib(10))
print(fib(20))
Вывод:
55 6765
Итог
- Дефолт вычисляется один раз при определении функции, а не на каждый вызов.
- Изменяемый дефолт (
[],{}) копит состояние между вызовами — это баг. - Правильно:
def f(x=None)и создание объекта внутри приx is None.
Проверьте себя
1. Когда вычисляется значение аргумента по умолчанию?
AПри каждом вызове функции
BОдин раз — при определении функции
CПри импорте модуля каждый раз
DНикогда
2. Как правильно задать изменяемый аргумент по умолчанию?
Adef f(x=[])
Bdef f(x={})
Cdef f(x=None) и создать объект внутри
DНикак нельзя
3. Что выведет третий вызов add(3) при def add(item, target=[])?
A[3]
B[1, 2, 3]
C[]
DОшибку