Почему пустой словарь в Python – опасное значение по умолчанию?

Пройдите тест, узнайте какой профессии подходите

Я предпочитаю
0%
Работать самостоятельно и не зависеть от других
Работать в команде и рассчитывать на помощь коллег
Организовывать и контролировать процесс работы

Быстрый ответ

Аргументы функции с присвоенными значениями по умолчанию, как dict(), создаются один раз при определении функции и повторно используются при каждом вызове. Такой подход может привести к непредвиденным ошибкам, так как любые изменения в этом объекте сохраняются и между последующими вызовами функции.

Вот наглядный пример:

Python
Скопировать код
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 в качестве значения по умолчанию и инициализировать словарь уже в теле функции:

Python
Скопировать код
def add_item(key, value, data=None):
    if data is None:
        data = {}
    data[key] = value
    return data

Такой подход гарантирует, что при каждом вызове будет создан новый словарь, и результаты вызовов функции не будут влиять друг на друга.

Кинга Идем в IT: пошаговый план для смены профессии

Риски применения изменяемых значений по умолчанию

Применение изменяемых объектов, таких как словари или списки, в качестве значений по умолчанию, может привести к сохранению изменений в результате различных вызовов функций. Изменения, внесенные во время одного вызова, как следствие, переходят к следующим, что затрудняет процесс отладки.

Основные принципы

  • Неизменяемые объекты как значения по умолчанию: безопасная и надежная практика, так как эти объекты не подвержены модификациям.
  • Изменяемые объекты как значения по умолчанию: потенциально не безопасно, так как их состояние сохраняется между вызовами функции.
  • Применение None в качестве значения по умолчанию: распространенный подход. С помощью None можно указать на отсутствие значения, которое должно быть инициализировано внутри функции.

Проверенные способы решения

  • Инициализируйте изменяемые объекты уже в теле функций.
  • В качестве значений по умолчанию применяйте неизменяемые объекты или None.
  • Если вы отклоняетесь от универсальных практик, обязательно документируйте код для лучшего понимания.

Защитные меры от проблем со значениями по умолчанию

Использование functools.partial

functools.partial позволяет создать функциональный объект с подготовленным изменяемым значением по умолчанию, которое перевычисляется при каждом вызове.

Python
Скопировать код
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 или аналогами.

Визуализация

Воспроизведение примера, демонстрирующего проблему с изменяемыми значениями по умолчанию. Рассмотрим случай с пустым словарем:

Markdown
Скопировать код
# Настройка: функция, которая кажется безобидной
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)

Результаты:

Markdown
Скопировать код
Первый вызов: {'a': 1}
Второй вызов: {'a': 1, 'b': 2}

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

Альтернативы и профилактика изменяемых значений по умолчанию

"None" – предпочтительнее

Аргументу присваивается значение по умолчанию None, а затем инициализируется в теле функции – это наиболее часто используемый метод.

Фабрики и вызываемые объекты

Можно применить фабрики или лямбда-функции, чтобы гарантировать создание нового объекта при каждом вызове функции.

Python
Скопировать код
def add_item(key, value, data=lambda: {}):
    data = data()
    data[key] = value
    return data

Конструкторы классов

Если условно тип имеет конструктор, его можно вызвать для создания нового экземпляра.

Обдуманное управление состоянием

Если вам требуется сохранить состояние между вызовами, предусмотрите методы для его сброса.

Python
Скопировать код
def add_item(key, value, data={}):
    if 'clear' in key:
        data.clear()
    else:
        data[key] = value
    return data

Полезные материалы

  1. "Least Astonishment" and the Mutable Default Argument — обсуждение осложненности изменяемых значений по умолчанию в Python.
  2. Defining Your Own Python Function – Real Python — подробное рассмотрение использования параметров по умолчанию и связанных с ними особенностей.
  3. Common Gotchas — The Hitchhiker's Guide to Python — изложение трудностей, связанных с изменяемыми параметрами по умолчанию в Python.
  4. Using a mutable default value as an argument — Python Anti-Patterns documentation — описание часто встречающихся ошибок при использовании изменяемых аргументов.