Если None не является допустимыми данными, используйте его как признак отсутствующего аргумента, проверяйте через is None и создавайте список внутри функции. Отдельно решите, нужно ли изменять или копировать явно переданный список.
Что останется в списке после двух вызовов
Запустите пример в новом процессе Python. Вывод: ['SQL'], затем ['SQL', 'Go'], затем True и снова ['SQL', 'Go']. Функция возвращает сам список history, а не снимок его содержимого. first и second ссылаются на один объект, поэтому второй вызов меняет и наблюдаемое значение first.
Список по умолчанию принадлежит конкретному объекту функции. Повторное выполнение def создаёт новую функцию с новым значением по умолчанию. Это важно для вложенных функций и экспериментов в notebook: повторный запуск определения может сбросить состояние и замаскировать проблему.
def remember(topic, history=[]):
history.append(topic)
return history
first = remember("SQL")
print(first)
second = remember("Go")
print(second)
print(first is second)
print(first)Создавайте список только при отсутствии аргумента
Исправленная функция печатает ['SQL'] ['Go'] False, затем ['Python'] True. Вызовы без history получают независимые списки. Когда вызывающий код явно передаёт owned, функция изменяет и возвращает именно этот объект — такой контракт выбран в примере.
Не заменяйте условие на if not history или history = history or []. Пустой список ложен в логическом контексте, но может быть намеренно переданным объектом. Его замена незаметно меняет поведение API. Если обещано не изменять данные вызывающего кода, явно копируйте переданную коллекцию и описывайте это решение.
def remember(topic, history=None):
if history is None:
history = []
history.append(topic)
return history
first = remember("SQL")
second = remember("Go")
print(first, second, first is second)
owned = []
result = remember("Python", owned)
print(owned, result is owned)Выбирайте значение по умолчанию под контракт
Проблема связана со временем жизни объекта и мутацией, а не с запретом любых значений, кроме None. Неизменяемые значения часто подходят. Намеренно общий кеш — тоже отдельный контракт, но его время жизни, очистку и конкурентный доступ нужно проектировать явно.
Если None является допустимым входом, отдельный sentinel object() позволяет отличить пропущенный аргумент от явного None. Сравнивайте его по идентичности. Аналогично рассуждайте о временной метке или случайном идентификаторе по умолчанию: значение нужно при создании функции или заново при каждом вызове?
| Требование | Приём | Проверка |
|---|---|---|
| Новый список при пропущенном аргументе | history=None; создание внутри | Результаты двух вызовов — разные объекты |
| Сохранить явно переданный пустой список | if history is None | Возвращён исходный объект |
| None — допустимые данные | Отдельный sentinel object() | Пропуск отличается от явного None |
| Новый список для каждого dataclass | field(default_factory=list) | Экземпляры не разделяют поле |
Для поля dataclass используйте default_factory
Dataclass описывает поля экземпляров, поэтому фабрика должна создавать коллекцию для каждого экземпляра, которому нужно значение по умолчанию. Передавайте сам list, а не list(): второй вариант сразу вызовет конструктор и передаст список вместо вызываемого объекта.
Пример печатает ['SQL'] [], затем False. Изменение first.topics не затрагивает second.topics. Современный dataclasses отклоняет многие изменяемые значения полей по умолчанию; используйте документированный механизм фабрики, а не рассчитывайте на детали проверки.
from dataclasses import dataclass, field
@dataclass
class ReviewSession:
topics: list[str] = field(default_factory=list)
first = ReviewSession()
second = ReviewSession()
first.topics.append("SQL")
print(first.topics, second.topics)
print(first.topics is second.topics)Проверяйте идентичность вместе с содержимым
Тест с единственным вызовом remember пройдёт и для сломанной функции. Сделайте два вызова без аргумента, проверьте независимое содержимое, затем измените один результат и убедитесь, что второй не поменялся. Отдельный тест с явно переданным пустым списком защитит выбранный контракт от замены is None на проверку истинности.
На собеседовании сначала объясните момент вычисления значения по умолчанию, затем вывод программы. Отдельно уточните, меняет ли функция объекты вызывающего кода. Это рассуждение переносится на словари, множества, накопители и вспомогательные функции сервиса. Повторяйте понятие в RecallDeck и проверяйте его небольшим исполняемым примером.
Коротко
Частые вопросы
Проблема касается словарей и множеств?
Да. Если функция изменяет словарь или множество по умолчанию, следующие вызовы без этого аргумента используют тот же изменённый объект функции.
Почему is None, а не == None?
Контракт требует проверки идентичности с единственным объектом None. Равенство может быть переопределено типом аргумента и для обнаружения такого маркера не нужно.
Достаточно ли копировать список по умолчанию в начале?
Это может предотвратить мутацию самого значения по умолчанию, но оставляет общий объект и усложняет понимание владения. Маркер и явное создание коллекции яснее выражают время жизни на один вызов.
Источники
Источники и редакционная политика
Сведения проверены 21 октября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.