PythonJuniorCoding

Что выведет def f(x, lst=[]): lst.append(x); return lst при двух последовательных вызовах и почему?

Выведет [1], затем [1, 2]. Дефолтный список создаётся один раз при выполнении def и сохраняется в f.__defaults__. Все вызовы без аргумента lst используют тот же объект, потому он накапливает добавления. Безопасный паттерн: lst=None и lst = lst if lst is not None else [].

Точный вывод и почему

def f(x, lst=[]):
    lst.append(x)
    return lst

print(f(1))    # [1]
print(f(2))    # [1, 2]   — тот же список!
print(f(3))    # [1, 2, 3]
print(f.__defaults__)   # ([1, 2, 3],) — сидит на функции

Причина: Python вычисляет default values один раз — в момент исполнения def (обычно при импорте модуля). Результаты сохраняются в кортеже func.__defaults__. При каждом вызове без переданного lst функция берёт ссылку на тот самый объект. lst.append мутирует его — изменение видно всем последующим вызовам.

Когда возникает

Ловушка срабатывает для любых mutable default:

  • def f(x, items=[])
  • def f(x, cfg={})
  • def f(x, seen=set())
  • def f(x, ts=datetime.now()) — здесь "ловушка" другая: datetime.now() вычисляется один раз и навсегда.

Безопасный паттерн

def f(x, lst=None):
    if lst is None:
        lst = []
    lst.append(x)
    return lst

print(f(1))    # [1]
print(f(2))    # [2]
print(f(3))    # [3]

Sentinel-вариант, когда None — валидное значение:

_MISSING = object()

def f(x, lst=_MISSING):
    if lst is _MISSING:
        lst = []
    lst.append(x)
    return lst

В dataclass и pydantic

from dataclasses import dataclass, field

@dataclass
class Cart:
    items: list = field(default_factory=list)
    # items: list = []     # ValueError при импорте

В pydantic v2 — то же через Field(default_factory=list).

Когда mutable default используют намеренно

Очень редко — как функция-кеш:

def expensive(arg, _cache={}):
    if arg not in _cache:
        _cache[arg] = compute(arg)
    return _cache[arg]

Работает, но functools.lru_cache читается лучше и поддерживает TTL/maxsize.

Подводные камни

  • Mutable default между вызовами — баг, который проявляется только при многократном вызове или в long-running процессе.
  • В тестах monkeypatch не помогает — default уже захвачен в __defaults__.
  • datetime.now() / uuid4() / os.environ["X"] в default — фиксируется на импорте, потом «не меняется».
  • В @dataclass mutable-default запрещён напрямую (ValueError) — забывать field(default_factory=...).
  • Линтер ruff B006 ловит mutable default — включите в CI.

Common mistakes

  • Говорить, что список создаётся при каждом вызове.
  • Исправлять через lst=[] внутри сигнатуры другой функции.
  • Не связывать поведение с defaults.

What the interviewer is testing

  • Предсказывает точный вывод двух вызовов.
  • Объясняет момент вычисления default arguments.
  • Показывает safe-паттерн через None или sentinel.

Sources

Related topics