Ловушка изменяемого аргумента по умолчанию

Любимый «вопрос с подвохом»: почему список-дефолт ведёт себя странно между вызовами.

Значение по умолчанию вычисляется один раз — в момент определения функции, а не при каждом вызове. Для изменяемого дефолта это значит общий объект на все вызовы.

Вопрос: что не так с 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Ошибку
Поддержать проект