← Назад к вопросам

Почему не стоит делать изменяемый аргумент функции по умолчанию в Python?

2.0 Middle🔥 251 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мутируемые аргументы по умолчанию в Python: опасная ошибка

Это классическая ошибка новичков, которая вызывает странные баги. За 10+ лет я видел эту проблему в коде бесчисленное количество раз. Расскажу, почему это происходит и как это исправить.

Проблема: неправильное поведение

# ПЛОХО! Не делай так!
def add_item(item, items_list=[]):
    items_list.append(item)
    return items_list

# Первый вызов
result1 = add_item("apple")
print(result1)  # ['apple']

# Второй вызов БЕЗ аргумента
result2 = add_item("banana")
print(result2)  # ['apple', 'banana'] — ВОТ ЭТО НЕОЖИДАННО!

# Третий вызов
result3 = add_item("cherry")
print(result3)  # ['apple', 'banana', 'cherry']

# А что если ты думаешь, что получишь новый список каждый раз?
list1 = add_item(1)
list2 = add_item(2)
list3 = add_item(3)

print(list1)  # [1, 2, 3] — все три значения в первом списке!
print(list1 is list2)  # True — это один и тот же объект!

Почему это происходит?

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

# Это происходит один раз при определении функции
def problematic_function(items=[]):
    # items указывает на конкретный объект в памяти
    # Этот объект НЕ создаётся заново при каждом вызове
    pass

# Наглядно:
def show_default(my_list=[]):
    print(f"ID объекта: {id(my_list)}")
    return my_list

print(id(show_default.__defaults__[0]))  # 12345
show_default()  # ID объекта: 12345
show_default()  # ID объекта: 12345 — ТОТ ЖЕ ОБЪЕКТ!

Примеры мутируемых типов данных

# Опасные типы (изменяемые)

def bad_list(items=[]):           # Опасно!
    pass

def bad_dict(config={}):          # Опасно!
    pass

def bad_set(unique_items=set()):  # Опасно!
    pass

# Безопасные типы (неизменяемые)

def safe_string(text="default"):     # OK
    pass

def safe_number(count=0):             # OK
    pass

def safe_tuple(pair=()):              # OK (но не используй как изменяемый)
    pass

def safe_none(value=None):            # Лучший вариант для мутируемых!
    pass

Правильное решение: используй None

# ХОРОШО!
def add_item(item, items_list=None):
    if items_list is None:
        items_list = []
    items_list.append(item)
    return items_list

# Первый вызов
result1 = add_item("apple")
print(result1)  # ['apple']

# Второй вызов
result2 = add_item("banana")
print(result2)  # ['banana'] — новый список!

# Третий вызов
result3 = add_item("cherry")
print(result3)  # ['cherry'] — снова новый!

print(result1 is result2)  # False — разные объекты!

Другой вариант: использование аннотаций типов

from typing import Optional, List

# Явно показывает, что используется mutable default
def process_data(data: Optional[List[str]] = None) -> List[str]:
    if data is None:
        data = []
    return data

# Более современный подход с dataclass
from dataclasses import dataclass, field

@dataclass
class DataProcessor:
    items: List[str] = field(default_factory=list)
    
    def add_item(self, item: str) -> None:
        self.items.append(item)

# Использование
processor1 = DataProcessor()
processor2 = DataProcessor()
processor1.add_item("apple")
print(processor1.items)  # ['apple']
print(processor2.items)  # [] — разные списки!

Ошибка с dict

# ПЛОХО!
def merge_configs(user_config, defaults={"debug": False}):
    defaults.update(user_config)
    return defaults

config1 = merge_configs({"timeout": 30})
config2 = merge_configs({"port": 8000})

print(config1)  # {'debug': False, 'timeout': 30, 'port': 8000}
print(config2)  # {'debug': False, 'timeout': 30, 'port': 8000}
# config1 и config2 — один и тот же объект!

# ХОРОШО!
def merge_configs(user_config, defaults=None):
    if defaults is None:
        defaults = {"debug": False}
    result = defaults.copy()  # Или используй copy.deepcopy()
    result.update(user_config)
    return result

Сложный пример: вложенные структуры

# Опасное поведение с вложенными данными
def build_tree(value, left=None, right=None):
    return {
        "value": value,
        "left": left or [],      # Опасно!
        "right": right or []
    }

tree1 = build_tree(1)
tree2 = build_tree(2)
tree1["left"].append("node")
print(tree2["left"])  # Может содержать "node" — BUG!

# Правильный способ
def build_tree_correct(value, left=None, right=None):
    return {
        "value": value,
        "left": left if left is not None else [],
        "right": right if right is not None else []
    }

Инструменты для поиска таких ошибок

# Flake8 плагин
pip install flake8-bugbear

# Проверяет B006: мутируемые аргументы по умолчанию
flake8 --select=B006 script.py

# Mypy с правильными аннотациями найдёт проблемы
mypy --strict script.py

# Pylint тоже поддерживает обнаружение
pylint script.py

Чеклист для code review

# Спрашивай себя при code review:

# ❌ ПЛОХО
def func1(items=[]):  # Список по умолчанию
    pass

def func2(config={}):  # Dict по умолчанию
    pass

def func3(data=set()):  # Set по умолчанию
    pass

# ✅ ХОРОШО
def func1(items=None):
    if items is None:
        items = []

def func2(config=None):
    if config is None:
        config = {}

def func3(data=None):
    if data is None:
        data = set()

# АЛЬТЕРНАТИВНО ХОРОШО (Python 3.10+)
def func1(items=None):
    items = items or []

def func2(config=None):
    config = config or {}

Почему это важно?

  1. Предсказуемость: функция работает так, как ожидается
  2. Отсутствие побочных эффектов: одного вызова не влияют на другие
  3. Тестируемость: легче писать тесты
  4. Отладка: баги сразу видны, а не приводят к странному поведению через несколько часов

Итоговое правило: НИКОГДА не используй изменяемые объекты как аргументы по умолчанию. Используй None и создавай новый объект внутри функции. Это одна из самых распространённых ошибок в Python, которую нужно помнить.