UpPetto

This is a real course generated by UpPetto — unedited.

AI-generated. Two models wrote it, a third checked every risky claim against sources, a fourth re-checked.

Create my own course

Изменяемое значение по умолчанию: главная ловушка бывшего 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
Изменяемый default создаётся один раз при определении функции и разделяется между всеми вызовами.

Разобранный пример

Функция с «кэшем», написанная по инерции в 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
[unconfirmed by second model] Значения аргументов по умолчанию вычисляются один раз, при определении функции (def)
Стандартная семантика Python; согласуется с разделом базы знаний о функциях, областях видимости и изменяемых значениях по умолчанию
Second model: База знаний не обсуждает время вычисления значений по умолчанию в Python.
[unconfirmed by second model] Ссылка на default-объект хранится в атрибуте f.__defaults__
Стандартный атрибут объекта функции; в базе знаний прямо не описан, но не противоречит ей — оставлено как способ интроспекции
Second model: База знаний не упоминает атрибут f.__defaults__ или хранение ссылок на объекты по умолчанию.
[softened] id() возвращает адрес объекта в памяти в CPython
База знаний говорит только об идентичности объектов и операторе is; формулировка изменена на 'уникальный идентификатор объекта'
[unconfirmed by second model] `is` — правильный способ сравнения с None (проверка идентичности, не равенства)
База знаний: идентичность проверяется оператором is, равенство — ==
Second model: База знаний не рассматривает сравнение с None через is или ==.
[unconfirmed by second model] Эффект проявляется только для изменяемых типов (list, dict, set)
Согласуется с разделом об изменяемых/неизменяемых объектах и ссылочной семантике
Second model: База знаний не обсуждает изменяемые значения по умолчанию и их специфику для mutable-типов.
[removed] Утверждение, что id() в CPython — это адрес объекта в памяти: софтнуто до 'уникальный идентификатор объекта' (база знаний говорит только об идентичности объектов)
[removed] Из диаграммы версии B удалены вымышленные значения id=12345; сама диаграмма дублирует сохранённую схему жизненного цикла default-значения

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.

Key concepts: mutable default argument статическое состояние identity
Tell me more 🔒 Didn't understand — explain simply 🔒 Show examples 🔒 Sources 🔒

On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course

Check yourself

1. Когда вычисляется выражение `[]` в `def f(x, cache=[])`?
2. Что произойдёт при многократном вызове `def log(message, history=[])` без передачи второго аргумента?
3. Какой вариант — идиоматичное исправление для `def fn(data, items=[])`?
On your own course, these are marked as you answer