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

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

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

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

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

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

Почему пустой список неправильно использовать как аргумент по умолчанию

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

Суть проблемы: Default Argument Evaluation

В Python значения по умолчанию вычисляются ровно один раз — при определении функции, а не при каждом её вызове.

# ❌ НЕПРАВИЛЬНО
def add_item(item, items=[]):
    items.append(item)
    return items

result1 = add_item(1)
print(result1)  # [1]

result2 = add_item(2)
print(result2)  # [1, 2] — ОЖИДАЛось [2]!

result3 = add_item(3)
print(result3)  # [1, 2, 3] — ОЖИДАЛОСЬ [3]!

# Все вызовы делят ОДИН И ТОТ ЖЕ объект списка!
print(result1 is result2 is result3)  # True

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

# Давайте посмотрим как Python это видит

def example_func(item, items=[]):
    items.append(item)
    return items

# Момент определения функции:
# items=[] — вычисляется один раз
# Этот список сохраняется в example_func.__defaults__

print(f"Функция __defaults__: {example_func.__defaults__}")
# ([], ) — кортеж с одним элементом

default_list = example_func.__defaults__[0]
print(f"ID списка: {id(default_list)}")

# Каждый вызов функции использует этот же объект
for i in range(3):
    result = example_func(i)
    print(f"ID в результате: {id(result)}")
    # Все ID будут одинаковые!

Демонстрация проблемы через объекты

# Можно проверить это с другими mutable типами

# ❌ Со списком
def bad_list(item, items=[]):
    items.append(item)
    return items

# ❌ Со словарём
def bad_dict(key, value, data={}):
    data[key] = value
    return data

# ❌ С set
def bad_set(item, items=set()):
    items.add(item)
    return items

print(bad_list(1))  # [1]
print(bad_list(2))  # [1, 2] — БАГИ!

print(bad_dict(a, 1))  # {a: 1}
print(bad_dict(b, 2))  # {a: 1, b: 2} — БАГ!

print(bad_set(1))  # {1}
print(bad_set(2))  # {1, 2} — БАГ!

Правильное решение

Используй None и инициализируй параметр внутри функции:

# ✅ ПРАВИЛЬНО
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

result1 = add_item(1)
print(result1)  # [1]

result2 = add_item(2)
print(result2)  # [2] — Теперь правильно!

result3 = add_item(3)
print(result3)  # [3]

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

Практические примеры

# ❌ Плохо
def register_user(name, tags=[]):
    tags.append(name)
    return tags

user1 = register_user(Alice)
user2 = register_user(Bob)
print(user1)  # [Alice, Bob] — БАГ!
print(user2)  # [Alice, Bob]

# ✅ Хорошо
def register_user(name, tags=None):
    if tags is None:
        tags = []
    tags.append(name)
    return tags

user1 = register_user(Alice)
user2 = register_user(Bob)
print(user1)  # [Alice]
print(user2)  # [Bob]
# ❌ Плохо: вложенные функции
def create_cache():
    def fetch_user(user_id, cache={}):
        if user_id not in cache:
            cache[user_id] = fetch_from_db(user_id)
        return cache[user_id]
    return fetch_user

# Будет внутренний кеш на весь процесс!

# ✅ Хорошо: кеш вне функции
def create_cache():
    cache = {}
    
    def fetch_user(user_id):
        if user_id not in cache:
            cache[user_id] = fetch_from_db(user_id)
        return cache[user_id]
    
    return fetch_user, cache  # Возвращаем обе
# ❌ Плохо: параметры с побочным эффектом
class User:
    def __init__(self, name, permissions=[]):
        self.name = name
        self.permissions = permissions  # Опасно!

user1 = User(Alice)
user1.permissions.append(admin)

user2 = User(Bob)
print(user2.permissions)  # [admin] — БАГ!

# ✅ Хорошо: копируем или инициализируем
class User:
    def __init__(self, name, permissions=None):
        self.name = name
        self.permissions = permissions.copy() if permissions else []

Когда это НЕ проблема

# ✅ Immutable объекты безопасны!
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))  # "Hello, Alice!"
print(greet("Bob"))    # "Hello, Bob!"

# ✅ Кортежи безопасны
def process(items, defaults=(1, 2, 3)):
    return items + defaults

print(process([]))  # [1, 2, 3]
print(process([]))  # [1, 2, 3]

# ✅ Numbers безопасны
def increment(n=0):
    return n + 1

print(increment())  # 1
print(increment())  # 1

Глубокое объяснение через bytecode

import dis

def bad_func(item, items=[]):
    items.append(item)
    return items

# Посмотрим на bytecode
dis.dis(bad_func)

# При определении функции:
# LOAD_CONST (пустой список [])
# BUILD_TUPLE (с другими defaults)
# MAKE_FUNCTION

# Пустой список создаётся один раз и сохраняется в константу!

def good_func(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

dis.dis(good_func)

# Здесь [] создаётся КАЖДЫЙ РАЗ в LOAD_CONST и BUILD_LIST

Антипаттерны в реальных проектах

# ❌ FastAPI эндпоинт (если вы не осторожны)
from fastapi import FastAPI

app = FastAPI()

# ❌ НЕПРАВИЛЬНО
@app.post("/add-item")
def add_item(item: str, history=[]):
    history.append(item)
    return {"history": history}  # Будет расти бесконечно!

# ✅ ПРАВИЛЬНО
@app.post("/add-item")
def add_item(item: str):
    history = [item]
    return {"history": history}
# ❌ Django ORM
def filter_users(status=active, exclude=None):
    if exclude is None:
        exclude = []
    return User.objects.filter(status=status).exclude(id__in=exclude)

# Если использовать exclude=[] как default, он будет разделяться!

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

# Проверьте эти паттерны:

# ❌ НАЙТИ И ЗАМЕНИТЬ:
from typing import List

def func1(x: List = []):  # ❌
    pass

def func2(d: dict = {}):  # ❌
    pass

def func3(s: set = set()):  # ❌
    pass

# ✅ ПРАВИЛЬНЫЙ ПАТТЕРН:
from typing import List, Optional

def func1(x: Optional[List] = None):  # ✅
    if x is None:
        x = []
    pass

def func2(d: Optional[dict] = None):  # ✅
    if d is None:
        d = {}
    pass

def func3(s: Optional[set] = None):  # ✅
    if s is None:
        s = set()
    pass

Почему это произошло в Python

Это было решением дизайна в CPython для оптимизации:

  • Значения по умолчанию вычисляются один раз
  • Это экономит время на каждый вызов функции
  • Это хорошо для immutable объектов
  • Но ловушка для mutable!

Теперь Python警告 об этом в документации, и это один из первых багов, который ловит статический анализатор типов.

Заключение

Золотое правило:

# ❌ НИКОГДА не используй mutable defaults:
# def func(x=[]):  # списки
# def func(x={}):  # словари  
# def func(x=set()):  # множества

# ✅ ВСЕГДА используй None:
# def func(x=None):
#     if x is None:
#         x = []

Этот баг настолько известен, что лучшие практики всегда рекомендуют использовать None для mutable параметров. Это убережёт вас от часов отладки странного поведения!