Изменяемый аргумент по умолчанию: значение лежит в поле функции, и оно одно на все вызовы
Правило «не пиши список в аргумент по умолчанию» запоминают все и всё равно нарушают, потому что оно выглядит произвольным запретом. Оно перестаёт быть произвольным, как только видно, где это значение лежит: в __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.
База: функция помнит то, чего помнить не должна
Начать стоит с ошибки, которую все узнают в лицо. Функция кладёт переданное в список и возвращает его:
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.
Значения параметров по умолчанию вычисляются слева направо при выполнении определения функции. Это значит, что выражение вычисляется один раз, когда функция определяется, и что одно и то же «предвычисленное» значение используется при каждом вызове.
«Предвычисленное значение» — не абстракция, его можно взять руками:
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.
Значения по умолчанию вычисляются в точке определения функции, в определяющей области видимости.
TIMEOUT = 5
def fetch(url, timeout=TIMEOUT):
...
TIMEOUT = 30 # fetch по-прежнему возьмёт 5И самое дорогое проявление:
import time
def log(message, at=time.time()):
... # at — момент импорта модуля, навсегдаТакая функция не сломается и не упадёт. Она просто будет писать одно и то же время во все записи, и заметят это тогда, когда логи понадобятся.
Механизм 3: починка и когда None не годится
Справочник предлагает решение сразу же:
def acc(item, into=None):
if into is None:
into = []
into.append(item)
return intoПоле функции при этом остаётся (None,) навсегда — None неизменяем, портить
в нём нечего.
Но None годится не всегда. Если None — допустимое значение аргумента,
проверка не может отличить «не передали» от «передали None»:
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: когда так делают нарочно
Одно применение у приёма всё-таки есть: словарь в аргументе по умолчанию — это состояние, живущее между вызовами.
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_cache | 46,26 |
lru_cache быстрее в 1,43 раза. Причина в том, что его обёртка написана на C
и делает один поиск в словаре, а тело на Python делает два — проверку not in и чтение — плюс сам вызов функции. Обёртка на C обошлась дешевле, чем
лишний поиск и вызов функции на Python.
Механизм 5: то же самое в классах
Механизм тот же, и документация датаклассов начинает объяснение именно с него:
Python stores default member variable values in class attributes
(Python хранит значения по умолчанию для переменных-членов в атрибутах класса).
class C:
items = [] # один список на все экземпляры
o1, o2 = C(), C()
o1.items.append(1)
print(o2.items) # [1]Датаклассы — единственная известная мне автоматическая проверка на эту ошибку в стандартной библиотеке. Граница у неё своя, и она уже, чем кажется:
@dataclass
class Bad:
items: list = []
# ValueError: mutable default <class 'list'> for field items
# is not allowed: use default_factoryПроверка стоит в самом декораторе и срабатывает при объявлении класса. Идёт
она по хешируемости, а не по изменяемости.
list, dict и set запрещены, а собственный класс с изменяемым состоянием
проходит молча — и экземпляры разделят один объект. В обычной функции такой
проверки нет вовсе: её делают только линтеры.
Глубже: что это стоит
Остался последний довод в защиту изменяемого значения по умолчанию — «зато без проверки быстрее». Довод верен: 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 — проверено запуском одного скрипта на четырёх версиях. Механизм не менялся и меняться не собирается: он описан в справочнике языка, а не в деталях реализации. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
def add(item, bucket=[]): bucket.append(item) return bucket print(add(1)) print(add(2)) print(add.__defaults__)
Практика · оцените
Проверьте себя
def acc(item, into=[]) вызвали трижды: acc(1), acc(2), acc(3). Что вернёт третий вызов и чему равно acc.__defaults__?
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Выражение после
=вычисляется один раз — при выполнении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. Проверяется счётчиком: функция, стоящая в значении по умолчанию, вызывается ровно один раз при объявлении, сколько бы раз потом ни вызывали ту функцию, в чьём значении по умолчанию она стоит. Отсюда иdef log(at=time.time()), замораживающий время импорта модуля. - Проблема в изменяемости, а не в типе. Тот же
def f(x=MyClass())сломается так же, если у объекта есть изменяемое состояние. И наоборот:def f(x=())илиdef f(x=0)безопасны не потому, что это «простые типы», а потому, что испортить их нечем. - Проверка стоит 4,74 нс. Разница между сломанным и правильным вариантом — 16 нс, и больше двух третей из них уходит на создание нового списка, то есть на работу, которой сломанный вариант не делает вовсе. Сравнивать их по скорости вообще некорректно: они возвращают разное.
- Она нужна ровно в одном случае, и он реальный: когда
None— допустимое значение аргумента. Тогда проверка наNoneне может отличить «не передали» от «передали None», и функция вродеget(key, default=None)перестаёт различать «ключа нет» и «значение по умолчанию — None». - Замерено: 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 — проверено запуском одного скрипта на четырёх версиях. Механизм не менялся и меняться не собирается: он описан в справочнике языка, а не в деталях реализации.<
Что разобрано
- База: функция помнит то, чего помнить не должна
- Механизм 1: где лежит значение по умолчанию
- Механизм 2: тот же механизм, вторая ловушка
- Механизм 3: починка и когда `None` не годится
- Механизм 4: когда так делают нарочно
- Механизм 5: то же самое в классах
- Глубже: что это стоит
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
5 ИСТОЧНИКОВ
- Справочник языка — определения функций, значения по умолчаниюОфициальная документация. Первоисточник, и он описывает не только механизм, но и саму ошибку, и способ её обойти: «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
- Модель данных — атрибут функции __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
- Учебник — значения по умолчанию вычисляются в определяющей областиОфициальная документация. Вторая половина того же механизма, о которой вспоминают реже: «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
- 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
- functools.lru_cacheОфициальная документация. Штатная замена самому осмысленному применению этого приёма — кешу в аргументе по умолчанию. Нужна здесь ради замера: прогретый lru_cache оказался быстрее самодельного кеша в 1,43 раза, хотя у него есть обёртка, а у самодельного нет.https://docs.python.org/3.14/library/functools.html#functools.lru_cache