Почему пустой словарь в Python – опасное значение по умолчанию?
Быстрый ответ
Аргументы функции с присвоенными значениями по умолчанию, как dict()
, создаются один раз при определении функции и повторно используются при каждом вызове. Такой подход может привести к непредвиденным ошибкам, так как любые изменения в этом объекте сохраняются и между последующими вызовами функции.
Вот наглядный пример:
def add_item(key, value, data={}):
data[key] = value
return data
add_item('a', 1) # {'a': 1} – все в порядке
add_item('b', 2) # {'a': 1, 'b': 2} – но вот и проблема
Чтобы избежать подобного, следует использовать None
в качестве значения по умолчанию и инициализировать словарь уже в теле функции:
def add_item(key, value, data=None):
if data is None:
data = {}
data[key] = value
return data
Такой подход гарантирует, что при каждом вызове будет создан новый словарь, и результаты вызовов функции не будут влиять друг на друга.
Риски применения изменяемых значений по умолчанию
Применение изменяемых объектов, таких как словари или списки, в качестве значений по умолчанию, может привести к сохранению изменений в результате различных вызовов функций. Изменения, внесенные во время одного вызова, как следствие, переходят к следующим, что затрудняет процесс отладки.
Основные принципы
- Неизменяемые объекты как значения по умолчанию: безопасная и надежная практика, так как эти объекты не подвержены модификациям.
- Изменяемые объекты как значения по умолчанию: потенциально не безопасно, так как их состояние сохраняется между вызовами функции.
- Применение
None
в качестве значения по умолчанию: распространенный подход. С помощьюNone
можно указать на отсутствие значения, которое должно быть инициализировано внутри функции.
Проверенные способы решения
- Инициализируйте изменяемые объекты уже в теле функций.
- В качестве значений по умолчанию применяйте неизменяемые объекты или
None
. - Если вы отклоняетесь от универсальных практик, обязательно документируйте код для лучшего понимания.
Защитные меры от проблем со значениями по умолчанию
Использование functools.partial
functools.partial
позволяет создать функциональный объект с подготовленным изменяемым значением по умолчанию, которое перевычисляется при каждом вызове.
from functools import partial
def add_item(key, value, data):
data[key] = value
return data
add_item_with_empty_dict = partial(add_item, data={})
Оборонительное программирование
Это необходимо при разработке библиотек, чтобы избежать непреднамеренного использвания общих изменяемых значений по умолчанию.
Всегда неизменяемые аргументы
Если нужно использовать неизменяемые словари, воспользуйтесь frozendict
или аналогами.
Визуализация
Воспроизведение примера, демонстрирующего проблему с изменяемыми значениями по умолчанию. Рассмотрим случай с пустым словарем:
# Настройка: функция, которая кажется безобидной
def add_to_dict(key, value, target_dict={}):
target_dict[key] = value
return target_dict
# Первое применение кажется безопасным
add_to_dict('a', 1)
# Но у второго вызова уже возникают проблемы
add_to_dict('b', 2)
Результаты:
Первый вызов: {'a': 1}
Второй вызов: {'a': 1, 'b': 2}
Один и тот же словарь используется при каждом вызове функции, поэтому модификации накапливаются.
Альтернативы и профилактика изменяемых значений по умолчанию
"None" – предпочтительнее
Аргументу присваивается значение по умолчанию None
, а затем инициализируется в теле функции – это наиболее часто используемый метод.
Фабрики и вызываемые объекты
Можно применить фабрики или лямбда-функции, чтобы гарантировать создание нового объекта при каждом вызове функции.
def add_item(key, value, data=lambda: {}):
data = data()
data[key] = value
return data
Конструкторы классов
Если условно тип имеет конструктор, его можно вызвать для создания нового экземпляра.
Обдуманное управление состоянием
Если вам требуется сохранить состояние между вызовами, предусмотрите методы для его сброса.
def add_item(key, value, data={}):
if 'clear' in key:
data.clear()
else:
data[key] = value
return data
Полезные материалы
- "Least Astonishment" and the Mutable Default Argument — обсуждение осложненности изменяемых значений по умолчанию в Python.
- Defining Your Own Python Function – Real Python — подробное рассмотрение использования параметров по умолчанию и связанных с ними особенностей.
- Common Gotchas — The Hitchhiker's Guide to Python — изложение трудностей, связанных с изменяемыми параметрами по умолчанию в Python.
- Using a mutable default value as an argument — Python Anti-Patterns documentation — описание часто встречающихся ошибок при использовании изменяемых аргументов.