Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·25 МИН

Изменяемый аргумент по умолчанию: значение лежит в поле функции, и оно одно на все вызовы

Правило «не пиши список в аргумент по умолчанию» запоминают все и всё равно нарушают, потому что оно выглядит произвольным запретом. Оно перестаёт быть произвольным, как только видно, где это значение лежит: в __defaults__ функции, созданное один раз при выполнении def.

Полное техническое изложение

TL;DR

Выражение после = вычисляется один раз — при выполнении def, а не при вызове. Получившийся объект функция хранит у себя и отдаёт каждому вызову, в котором этот аргумент не передан явно. Передали своё значение — хранимый объект не при чём. А если объект изменяемый и функция его меняет, изменения остаются в нём и достаются следующему вызову: правило про список в аргументе по умолчанию перестаёт быть произвольным запретом ровно здесь.

Отсюда главное следствие: функция накапливает состояние, которого никто не заводил, — и это состояние видно снаружи. Хранимое значение лежит в поле функции __defaults__ (у параметров только по имени — в __kwdefaults__), его можно прочитать, и список в нём растёт от вызова к вызову. Тот же момент вычисления даёт вторую ловушку, где изменяемых объектов нет вовсе: def log(at=time.time()) замораживает время импорта модуля навсегда, а def f(x=CONST) берёт значение CONST на момент объявления и не заметит переприсваивания. Починка — None и явная проверка; своя метка вместо None нужна ровно там, где None — допустимое значение аргумента.

какие значения по умолчаниюгде лежатв чём
позиционные__defaults__кортеж
только-по-имени__kwdefaults__словарь

Дальше — то, что отличает знающего от читавшего. Цена правильного варианта: 48,78 нс против 32,67 (3.13.7). Но больше двух третей разницы — это создание нового списка, то есть та самая работа, ради которой всё и пишется; сама проверка стоит 4,74 нс. Датаклассы эту ошибку запрещают прямо: x: list = [] падает с ValueError при объявлении класса — правда, граница проверки идёт по хешируемости, а не по изменяемости. И один замер вышел против ожидания: прогретый lru_cache быстрее самодельного кеша в аргументе по умолчанию в 1,43 раза — 46,26 нс против 66,09.

Порог входа
Перед уроком достаточно понимать
  • у параметра функции может быть значение по умолчанию, и тогда аргумент разрешено не передавать;
  • список и словарь можно изменить, не создавая новый объект, — а число и строку нельзя;
  • def — это выполняемая строка программы, а не объявление, которое компилятор просто принимает к сведению.
Заранее знать не нужно
  • __defaults__, __kwdefaults__, объект-метка вместо None;
  • датаклассы и default_factory, functools.lru_cache.

База: функция помнит то, чего помнить не должна

Начать стоит с ошибки, которую все узнают в лицо. Функция кладёт переданное в список и возвращает его:

PYTHON
def acc(item, into=[]):
    into.append(item)
    return into
 
print(acc(1))       # [1]
print(acc(2))       # [1, 2] — а ждали [2]

Второй вызов вернул список из двух элементов, хотя ему передали один. Отсюда и правило, которое запоминают все: не пиши список в аргумент по умолчанию.

Но в таком виде правилу нечем помочь: оно выглядит произвольным запретом, поэтому его и нарушают. Вопрос, на который отвечает урок, — не «что нельзя», а когда вычисляется то, что стоит после знака равенства. Если при каждом вызове, то поведение выше необъяснимо. Если один раз, при выполнении def, — то объяснимо всё, включая случаи, где никаких списков нет вовсе.

Верен второй вариант: значение вычисляется один раз, и получившийся объект достаётся каждому вызову, в котором этот аргумент не передан. Список никуда не исчезает между вызовами, потому что он там один.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, где именно лежит этот объект и как его увидеть, что ещё ломается по той же причине и сколько на самом деле стоит правильный вариант.

Механизм 1: где лежит значение по умолчанию

контракт языкаГарантия языка: выражение по умолчанию вычисляется один раз, при выполнении def. Это правило языка, а не особенность CPython.

Справочник описывает механизм в двух предложениях — и этого достаточно, чтобы вывести всё остальное.

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same "pre-computed" value is used for each call.

перевод

Значения параметров по умолчанию вычисляются слева направо при выполнении определения функции. Это значит, что выражение вычисляется один раз, когда функция определяется, и что одно и то же «предвычисленное» значение используется при каждом вызове.

Справочник языка, определения функций

«Предвычисленное значение» — не абстракция, его можно взять руками:

PYTHON
def acc(item, into=[]):
    into.append(item)
    return into
 
print(acc.__defaults__)     # ([],)
acc(1)
acc(2)
print(acc.__defaults__)     # ([1, 2],)

Поле функции растёт вместе со списком. Никакой особой магии в поломке нет: список создан один раз, положен в поле и оттуда достаётся каждому вызову, в котором этот аргумент не передан явно. Вызов acc(3, []) в этой картине не участвует вовсе — он работает со своим списком, а поле остаётся прежним.

Механизм 2: тот же механизм, вторая ловушка

Про изменяемость помнят чаще, чем про момент вычисления. А он тот же самый.

The default values are evaluated at the point of function definition in the defining scope.

перевод

Значения по умолчанию вычисляются в точке определения функции, в определяющей области видимости.

Учебник, значения аргументов по умолчанию
PYTHON
TIMEOUT = 5
 
def fetch(url, timeout=TIMEOUT):
    ...
 
TIMEOUT = 30        # fetch по-прежнему возьмёт 5

И самое дорогое проявление:

PYTHON
import time
 
def log(message, at=time.time()):
    ...            # at — момент импорта модуля, навсегда

Такая функция не сломается и не упадёт. Она просто будет писать одно и то же время во все записи, и заметят это тогда, когда логи понадобятся.

Механизм 3: починка и когда None не годится

Справочник предлагает решение сразу же:

PYTHON
def acc(item, into=None):
    if into is None:
        into = []
    into.append(item)
    return into

Поле функции при этом остаётся (None,) навсегда — None неизменяем, портить в нём нечего.

Но None годится не всегда. Если Noneдопустимое значение аргумента, проверка не может отличить «не передали» от «передали None»:

PYTHON
MISSING = object()
store = {"k": None}          # словарь, в котором ищем
 
def get(key, default=MISSING):
    value = store.get(key, MISSING)
    if value is MISSING:
        if default is MISSING:
            raise KeyError(key)
        return default
    return value

Здесь get("k", None) обязан вернуть None, а get("k") — возбудить KeyError, и различить их можно только своим объектом-меткой. Метка дороже None на 8,07 нс — не из-за сравнения, а из-за загрузки: None компилируется в константу, а метка лежит в глобальных именах модуля. Выбирают её, разумеется, не из-за этих восьми наносекунд, а потому что None в такой функции занят.

Механизм 4: когда так делают нарочно

Одно применение у приёма всё-таки есть: словарь в аргументе по умолчанию — это состояние, живущее между вызовами.

PYTHON
def fib(n, _cache={0: 0, 1: 1}):
    if n not in _cache:
        _cache[n] = fib(n - 1) + fib(n - 2)
    return _cache[n]

Работает. Но у этого решения три беды, и третья оказалась неожиданной.

Первые две очевидны: очистить кеш снаружи можно только через fib.__defaults__, а любой, кто вызывает fib, может подменить кеш вторым аргументом — случайно или нарочно.

Третья — скорость, и ожидание здесь было обратным. Казалось, что самодельный кеш быстрее functools.lru_cache: у того есть обёртка, а у этого нет. На прогретом кеше вышло наоборот:

кешнс на попадание
словарь в аргументе по умолчанию66,09
functools.lru_cache46,26

lru_cache быстрее в 1,43 раза. Причина в том, что его обёртка написана на C и делает один поиск в словаре, а тело на Python делает два — проверку not in и чтение — плюс сам вызов функции. Обёртка на C обошлась дешевле, чем лишний поиск и вызов функции на Python.

Механизм 5: то же самое в классах

Механизм тот же, и документация датаклассов начинает объяснение именно с него: Python stores default member variable values in class attributes (Python хранит значения по умолчанию для переменных-членов в атрибутах класса).

PYTHON
class C:
    items = []          # один список на все экземпляры
 
o1, o2 = C(), C()
o1.items.append(1)
print(o2.items)         # [1]

Датаклассы — единственная известная мне автоматическая проверка на эту ошибку в стандартной библиотеке. Граница у неё своя, и она уже, чем кажется:

PYTHON
@dataclass
class Bad:
    items: list = []
# ValueError: mutable default <class 'list'> for field items
#             is not allowed: use default_factory

Проверка стоит в самом декораторе и срабатывает при объявлении класса. Идёт она по хешируемости, а не по изменяемости. list, dict и set запрещены, а собственный класс с изменяемым состоянием проходит молча — и экземпляры разделят один объект. В обычной функции такой проверки нет вовсе: её делают только линтеры.

Глубже: что это стоит

наблюдение замераbench/defaults/cost.py, CPython 3.13.7. Разница между часовым и изменяемым значением по умолчанию — наносекунды; выбор делается не по ним.

Остался последний довод в защиту изменяемого значения по умолчанию — «зато без проверки быстрее». Довод верен: 32,67 нс против 48,78, то есть на 49 % дороже. Разложение показывает, за что именно платят:

  • 11,37 нс — создание нового списка. Это не накладной расход, а работа, ради которой правильный вариант и пишется: сломанный дешевле ровно потому, что списка не создаёт.
  • 4,74 нс — сама проверка is None.

То есть настоящая цена самой проверки — пять наносекунд на вызов. Остальные одиннадцать — новый список, которого сломанный вариант просто не создаёт.

И граница у этих чисел та же, что у любого замера: они сняты на одной машине и на одной версии. Переносится отсюда не «49 %», а разложение — из чего эта разница состоит и почему сравнивать два варианта по скорости вообще некорректно: они возвращают разное.

Как отвечать на собеседовании

Короткий ответ: выражение после = вычисляется один раз, при выполнении def, и получившийся объект достаётся каждому вызову, в котором этот аргумент не передан. Поэтому изменяемое значение по умолчанию накапливает изменения между вызовами. Чинится проверкой на None; своя метка нужна только там, где None — допустимое значение аргумента.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Что отличает хороший ответ: показать f.__defaults__ — это превращает правило в следствие. Значение лежит в поле функции, поле видно снаружи, и список в нём растёт от вызова к вызову; правило после этого не надо помнить — оно выводится.

И назвать вторую половину механизма: значение вычисляется в момент def, поэтому def log(at=time.time()) ломается по той же причине, хотя изменяемых объектов там нет вовсе.

Стоит держать точность и в самой формулировке. Не «одно значение на все вызовы», а один и тот же объект достаётся каждому вызову, в котором этот аргумент не передан явно: стоит передать своё значение — и поле функции в деле не участвует. Ошибка живёт не в списке и не в вызове, а на их пересечении.

Дальше спросят

Спросят дальше

Починили через None. Так можно всегда?

Короткий ответ

Не всегда: None работает как признак «значение не передано» только пока сам None не является допустимым значением параметра. Там, где он допустим, нужен отдельный объект-страж, иначе починка тихо меняет смысл вызова.

Спросят дальше

А если такое поведение нужно нарочно?

Короткий ответ

Так и делают — это способ приклеить состояние к функции, и он законен. Разница не в механизме, а в намеренности: одно и то же поведение бывает и приёмом, и ошибкой, и отличает их только то, знал ли автор.

Спросят дальше

В классах то же самое?

Короткий ответ

Да, и там ловушка та же по механизму: значение вычисляется один раз в момент выполнения объявления и дальше живёт общим для всех, кто его не переопределил.

Частые заблуждения

Утверждение

Значение по умолчанию вычисляется при каждом вызове

На самом деле

Один раз, при выполнении def. Проверяется счётчиком: функция, стоящая в значении по умолчанию, вызывается ровно один раз при объявлении, сколько бы раз потом ни вызывали ту функцию, в чьём значении по умолчанию она стоит. Отсюда и def log(at=time.time()), замораживающий время импорта модуля.

Утверждение

Проблема в списках и словарях

На самом деле

Проблема в изменяемости, а не в типе. Тот же def f(x=MyClass()) сломается так же, если у объекта есть изменяемое состояние. И наоборот: def f(x=()) или def f(x=0) безопасны не потому, что это «простые типы», а потому, что испортить их нечем.

Утверждение

Правильный вариант заметно дороже: приходится каждый раз проверять is None

На самом деле

Проверка стоит 4,74 нс. Разница между сломанным и правильным вариантом — 16 нс, и больше двух третей из них уходит на создание нового списка, то есть на работу, которой сломанный вариант не делает вовсе. Сравнивать их по скорости вообще некорректно: они возвращают разное.

Утверждение

Своя метка вместо None — усложнение на ровном месте

На самом деле

Она нужна ровно в одном случае, и он реальный: когда None — допустимое значение аргумента. Тогда проверка на None не может отличить «не передали» от «передали None», и функция вроде get(key, default=None) перестаёт различать «ключа нет» и «значение по умолчанию — None».

Утверждение

Кеш в аргументе по умолчанию быстрее lru_cache: у того обёртка

На самом деле

Замерено: 66,09 нс против 46,26 на прогретом кеше — lru_cache быстрее в 1,43 раза. Ожидание было обратным. Его обёртка написана на C и делает ОДИН поиск в словаре, а тело на Python делает два — not in и чтение — плюс сам вызов функции.

История версий

ВерсияИзменениеЧто это значит для кода
3.0Появляются параметры, передаваемые только по имени, и вместе с ними второе поле — __kwdefaults__. Механизм тот же: словарь создаётся один раз при выполнении def, и изменяемое значение в нём ведёт себя ровно так же, как в __defaults__.
3.7Появляются датаклассы (PEP 557) — и вместе с ними автоматическая проверка, которая эту ошибку ловит (в тех случаях, что описаны выше: граница идёт по хешируемости). x: list = [] в теле датакласса возбуждает ValueError при ОБЪЯВЛЕНИИ класса, а не при первом вызове. Текст сообщения одинаков на 3.11–3.14.
3.11Опорная точка урока. Содержимое __defaults__ до и после вызовов, момент вычисления значения и текст ошибки датакласса на 3.11 такие же, как на 3.14.7 — проверено запуском одного скрипта на четырёх версиях. Механизм не менялся и меняться не собирается: он описан в справочнике языка, а не в деталях реализации.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Значение по умолчанию — пустой список. Функция вызывается дважды, а потом печатается __defaults__. Что напечатает этот код?
def add(item, bucket=[]):
  bucket.append(item)
  return bucket


print(add(1))
print(add(2))
print(add.__defaults__)

Практика · оцените

Кеш квадратов, написанный словарём в аргументе по умолчанию, против functools.lru_cache — оба прогреты. Во сколько раз lru_cache БЫСТРЕЕ?
раза

Проверьте себя

Вопрос 1 из 5

def acc(item, into=[]) вызвали трижды: acc(1), acc(2), acc(3). Что вернёт третий вызов и чему равно acc.__defaults__?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

5 ИСТОЧНИКОВ

  1. Справочник языка — определения функций, значения по умолчаниюОфициальная документация. Первоисточник, и он описывает не только механизм, но и саму ошибку, и способ её обойти: «Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call» (Значения параметров по умолчанию вычисляются слева направо при выполнении определения функции. Это значит, что выражение вычисляется ОДИН раз, когда функция определяется, и что одно и то же „предвычисленное“ значение используется при каждом вызове). И далее прямо про изменяемые объекты: «if the function modifies the object (e.g. by appending an item to a list), the default parameter value is in effect modified. This is generally not what was intended» (если функция изменяет объект (например, добавляя элемент в список), значение параметра по умолчанию оказывается изменённым. Обычно это не то, что имелось в виду).https://docs.python.org/3.14/reference/compound_stmts.html#function-definitions
  2. Модель данных — атрибут функции __defaults__Официальная документация. Где именно лежит это значение: «A tuple containing default parameter values for those parameters that have defaults, or None if no parameters have a default value» (Кортеж, содержащий значения по умолчанию для тех параметров, у которых они есть, либо None, если ни у одного параметра значения по умолчанию нет). Рядом описан __kwdefaults__ — отдельное поле для параметров, передаваемых только по имени. Именно чтение этих полей и превращает правило в следствие: список из __defaults__ виден снаружи и растёт от вызова к вызову.https://docs.python.org/3.14/reference/datamodel.html#special-read-only-attributes
  3. Учебник — значения по умолчанию вычисляются в определяющей областиОфициальная документация. Вторая половина того же механизма, о которой вспоминают реже: «The default values are evaluated at the point of function definition in the defining scope» (Значения по умолчанию вычисляются в точке определения функции, в определяющей области видимости). Там же пример с i = 5 и последующим i = 6, где функция печатает 5. Отсюда и ловушка с def log(at=time.time()).https://docs.python.org/3.14/tutorial/controlflow.html#default-argument-values
  4. dataclasses — изменяемые значения по умолчаниюОфициальная документация. Единственная известная автоматическая проверка на эту ошибку в стандартной библиотеке; её граница идёт по хешируемости, а не по изменяемости. Документация начинает объяснение с того же механизма в классах: «Python stores default member variable values in class attributes» (Python хранит значения по умолчанию для переменных-членов в атрибутах класса), приводит пример, где o1.x is o2.x, и показывает, что датакласс на таком объявлении возбуждает ValueError. Оттуда же default_factory: «it must be a zero-argument callable that will be called when a default value is needed for this field» (это должен быть вызываемый объект без аргументов, который будет вызван, когда для этого поля понадобится значение по умолчанию).https://docs.python.org/3.14/library/dataclasses.html#mutable-default-values
  5. functools.lru_cacheОфициальная документация. Штатная замена самому осмысленному применению этого приёма — кешу в аргументе по умолчанию. Нужна здесь ради замера: прогретый lru_cache оказался быстрее самодельного кеша в 1,43 раза, хотя у него есть обёртка, а у самодельного нет.https://docs.python.org/3.14/library/functools.html#functools.lru_cache