Изменяемое значение по умолчанию: главная ловушка бывшего C-программиста
core найти в чужом коде функцию с изменяемым значением по умолчанию и объяснить, к какому багу это приведёт при повторных вызовах
В C статическая переменная внутри функции — это осознанный акт: вы пишете static int counter = 0; и явно понимаете, что она переживёт вызов и будет общей для всех обращений к функции. Слово static стоит прямо в коде и предупреждает читателя.
В Python есть механизм с точно таким же эффектом, но без единого сигнального слова — значения аргументов по умолчанию. Ключевая деталь, которую си-программист пропускает по инерции:
Выражение в def f(x, cache=[]) вычисляется один раз — в момент определения функции (def), а не при каждом вызове.
Когда интерпретатор встречает def, он создаёт объект-функцию и тут же вычисляет выражения для значений по умолчанию. Список [] создаётся один раз и становится частью объекта функции — он живёт ровно столько, сколько живёт сама функция (посмотреть его текущее состояние можно через f.__defaults__).
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — не [2]!
print(add_item(3)) # [1, 2, 3]
Каждый вызов без явного bucket получает тот же самый объект списка — не копию, не новый пустой список. Это ближе к статической переменной в C, чем к «параметру по умолчанию» в интуитивном смысле (в C default-параметров нет вовсе, но если бы были, естественно было бы ожидать создание нового значения на каждый вызов).
Проблема специфична именно для изменяемых (mutable) объектов — list, dict, set. Если default — число, строка или tuple (неизменяемые объекты), эффект не проявляется: их нельзя мутировать на месте, любая «модификация» создаёт новый объект.
Диагностика в чужом коде — ищите сигнатуру: def f(x, arg=[]), def f(x, arg={}), def f(x, arg=set()). Любой изменяемый литерал в default — потенциальная бомба. Проверить экспериментально можно через id() (уникальный идентификатор объекта) или оператор is (проверка идентичности, а не равенства):
def probe(d={}):
return id(d)
print(probe() == probe()) # True — один и тот же объект!
Идиоматическое исправление — сентинел None и создание нового объекта внутри тела функции:
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Теперь каждый вызов без явного bucket получает свежий список, а явная передача существующего списка продолжает работать как обычно.
flowchart LR
A["def f(x, cache=[])"] -->|выполняется один раз| B["[] создаётся при определении функции"]
B --> C["ссылка на список хранится в f.__defaults__"]
C -->|вызов 1| D["f(1) — мутирует общий список"]
C -->|вызов 2| E["f(2) — мутирует тот же список"]
D --> F["один и тот же объект видят оба вызова"]
E --> FРазобранный пример
Функция с «кэшем», написанная по инерции в C-стиле:
def process_data(data, cache={}):
if data in cache:
return cache[data]
result = data.upper() # представим тяжёлое вычисление
cache[data] = result
return result
print(process_data("alpha")) # ALPHA — вычислено
print(process_data("beta")) # BETA — вычислено
print(process_data("alpha")) # ALPHA — взято из кэша ПРЕДЫДУЩЕГО вызова
Автор думал о cache как о временном хранилище на один вызов, но {} создан один раз при def, поэтому все вызовы без явного аргумента используют общий словарь. Состояние «протекает» между логически независимыми вызовами.
Исправленная версия — сентинел None; при этом возможность передать долгоживущий кэш явно сохраняется:
def process_data_correct(data, cache=None):
if cache is None: # `is` — проверка идентичности, правильный способ сравнить с None
cache = {}
if data in cache:
return cache[data]
result = data.upper()
cache[data] = result
return result
session = {}
process_data_correct("alpha", cache=session) # кэш живёт в session
print(process_data_correct("alpha")) # свежий кэш, ничего не «протекает»
Попробуй сейчас
Найдите баг и перепишите функцию так, чтобы каждый вызов без второго аргумента получал независимый список:
def group_by_status(item, groups=[]):
groups.append(item)
return groups
Проверьте исправление: два последовательных вызова group_by_status("a") и group_by_status("b") без явного groups должны вернуть ["a"] и ["b"] по отдельности, а не ["a", "b"].
Получилось, если…
Вы разобрались, если можете:
1. За 10 секунд просканировать сигнатуру и найти изменяемый default ([], {}, set()).
2. Без запуска кода сказать: «этот объект один на все вызовы, будет накапливать состояние».
3. Предложить фикс: arg=None + if arg is None: arg = [].
4. Объяснить, почему два результата подряд — это ссылки на один и тот же объект списка.
Вывод
Значения аргументов по умолчанию вычисляются один раз — при определении функции, не при вызове. Изменяемый default ведёт себя как неявная статическая переменная. Правило: default всегда None или неизменяемый объект, реальный изменяемый объект создаётся внутри тела функции.
AI-generated · source-grounded review
🛡 Fact-checked: 0 risky claims verified · 2 removed · confidence: high · figures: 1
A second model re-checked this lesson's claims and corroborated 0 of 5 , and could not confirm 5 either way. This is incomplete corroboration, not a disagreement.
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course