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

Приведи пример логической ошибки, из-за неучтенного фактора изменяемости

1.3 Junior🔥 211 комментариев
#Python Core

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

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

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

Логические ошибки изменяемости (Mutability): реальные примеры из боя

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

Пример 1: Значение по умолчанию (список/словарь)

Задача: Функция, которая добавляет пользователя в список друзей.

Неправильный код:

class User:
    def __init__(self, name, friends=[]):
        """ОШИБКА: список друзей является изменяемым по умолчанию"""
        self.name = name
        self.friends = friends
    
    def add_friend(self, friend_name):
        self.friends.append(friend_name)

user1 = User("Alice")
user1.add_friend("Bob")
print(f"Друзья Alice: {user1.friends}")  # [Bob]

user2 = User("Charlie")
print(f"Друзья Charlie: {user2.friends}")  # [Bob] — ЧТО?!

Вывод:

Друзья Alice: [Bob]
Друзья Charlie: [Bob]  <- ОПА! А где-то тот же список!

Почему так происходит:

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

class User:
    # friends=[] создаётся ЗДЕСЬ один раз и переиспользуется
    def __init__(self, name, friends=[]):
        # Все экземпляры без явного friends получат ОДИН И ТОТ ЖЕ список
        self.friends = friends

Правильный код:

class User:
    def __init__(self, name, friends=None):
        self.name = name
        self.friends = friends if friends is not None else []
    
    def add_friend(self, friend_name):
        self.friends.append(friend_name)

user1 = User("Alice")
user1.add_friend("Bob")
print(f"Друзья Alice: {user1.friends}")  # [Bob]

user2 = User("Charlie")
print(f"Друзья Charlie: {user2.friends}")  # [] — правильно!

Пример 2: Присваивание списков (aliasing)

Задача: Копировать параметры конфигурации.

Неправильный код:

class Config:
    def __init__(self, settings):
        self.settings = settings  # Просто присваиваем ссылку!
    
    def update_setting(self, key, value):
        self.settings[key] = value

# Исходные настройки
original_settings = {"debug": False, "timeout": 30}

# Создаём конфиг
config = Config(original_settings)
config.update_setting("debug", True)

print(f"Исходные: {original_settings}")  # {debug: True, timeout: 30}
print(f"Конфиг: {config.settings}")      # {debug: True, timeout: 30}

Вывод:

Исходные: {"debug": True, "timeout": 30}  <- ИЗМЕНИЛСЯ!
Конфиг: {"debug": True, "timeout": 30}

Проблема: Оба переменные указывают на ОДИН И ТОТ ЖЕ объект в памяти. Изменение через один влияет на другой.

Правильный код:

class Config:
    def __init__(self, settings):
        # Делаем КОПИЮ вместо ссылки
        self.settings = settings.copy()  # Или deepcopy() для вложенных структур
    
    def update_setting(self, key, value):
        self.settings[key] = value

original_settings = {"debug": False, "timeout": 30}
config = Config(original_settings)
config.update_setting("debug", True)

print(f"Исходные: {original_settings}")  # {debug: False, timeout: 30} — ОК!
print(f"Конфиг: {config.settings}")      # {debug: True, timeout: 30}

Пример 3: Вложенные структуры (shallow vs deep copy)

Задача: Копировать матрицу чисел.

Неправильный код:

# Shallow copy — копируется только первый уровень
original_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
copy_matrix = original_matrix.copy()  # Только shallow copy!

# Изменяем скопированную матрицу
copy_matrix[0][0] = 999

print(f"Оригинал: {original_matrix}")      # [[999, 2, 3], [4, 5, 6], [7, 8, 9]]
print(f"Копия: {copy_matrix}")             # [[999, 2, 3], [4, 5, 6], [7, 8, 9]]

Вывод:

Оригинал: [[999, 2, 3], [4, 5, 6], [7, 8, 9]]  <- ИЗМЕНИЛСЯ!
Копия: [[999, 2, 3], [4, 5, 6], [7, 8, 9]]

Почему:

Shallow copy копирует только первый уровень. Внутренние списки остаются ссылками на те же объекты:

# Визуально
original_matrix ──┐
                  └─→ [список 1]  ←── copy_matrix тоже
copy_matrix ──┐
              └─→ [список 1]

# Когда меняем в copy_matrix, меняем shared список!

Правильный код:

import copy

original_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
copy_matrix = copy.deepcopy(original_matrix)  # Deep copy!

copy_matrix[0][0] = 999

print(f"Оригинал: {original_matrix}")  # [[1, 2, 3], [4, 5, 6], [7, 8, 9]] — ОК!
print(f"Копия: {copy_matrix}")         # [[999, 2, 3], [4, 5, 6], [7, 8, 9]]

Пример 4: Изменение списка во время итерации

Задача: Удалить все "пустые" значения из списка.

Неправильный код:

items = [1, None, 2, None, 3, None, 4]

# ОШИБКА: изменяем список во время итерации
for i, item in enumerate(items):
    if item is None:
        items.pop(i)  # Удаляем элемент

print(items)  # [1, 2, None, 3, None, 4] — остались None!

Вывод:

[1, 2, None, 3, None, 4]  <- Остались None элементы!

Почему ошибка:

Когда удаляем элемент на индексе i, все последующие элементы сдвигаются влево. Но iterаtor продолжает идти дальше, пропуская некоторые элементы:

Шаг 1: items = [1, None, 2, ...], i=1
       Удалили items[1] (None)
       items = [1, 2, ...]

Шаг 2: Итератор переходит на i=2, но это теперь не None, а 2!
       None на позиции 2 в новом списке пропущена!

Правильные решения:

Вариант 1: Итерируем по копии

items = [1, None, 2, None, 3, None, 4]

for item in items.copy():  # Итерируем по КОПИИ
    if item is None:
        items.remove(item)  # Удаляем из оригинала

print(items)  # [1, 2, 3, 4] — правильно!

Вариант 2: List comprehension (предпочтительно)

items = [1, None, 2, None, 3, None, 4]
items = [item for item in items if item is not None]
print(items)  # [1, 2, 3, 4]

Вариант 3: Обратная итерация

items = [1, None, 2, None, 3, None, 4]

for i in range(len(items) - 1, -1, -1):  # Идём в обратном порядке
    if items[i] is None:
        items.pop(i)

print(items)  # [1, 2, 3, 4]

Пример 5: Мутирующие методы

Задача: Отсортировать список и вернуть результат.

Неправильный код:

class NumberSorter:
    def __init__(self, numbers):
        self.numbers = numbers
    
    def get_sorted(self):
        """Сортирует и возвращает. Но что ещё?"""
        self.numbers.sort()  # Мутирует исходный список!
        return self.numbers

original = [5, 2, 8, 1]
sorter = NumberSorter(original)
result = sorter.get_sorted()

print(f"Исходный: {original}")  # [1, 2, 5, 8] — ИЗМЕНИЛСЯ!
print(f"Результат: {result}")   # [1, 2, 5, 8]

Вывод:

Исходный: [1, 2, 5, 8]  <- ОН ИЗМЕНИЛСЯ!
Результат: [1, 2, 5, 8]

Правильный код:

class NumberSorter:
    def __init__(self, numbers):
        self.numbers = numbers
    
    def get_sorted(self):
        """Сортирует и возвращает БЕЗ изменения оригинала"""
        return sorted(self.numbers)  # Возвращает НОВЫЙ список

original = [5, 2, 8, 1]
sorter = NumberSorter(original)
result = sorter.get_sorted()

print(f"Исходный: {original}")  # [5, 2, 8, 1] — не изменился
print(f"Результат: {result}")   # [1, 2, 5, 8]

Пример 6: Общее состояние в классе (class variable)

Задача: Отслеживать общее количество юзеров.

Неправильный код:

class User:
    all_users = []  # Класс переменная — ОБЩАЯ для всех экземпляров!
    
    def __init__(self, name):
        self.name = name
        User.all_users.append(self)  # Добавляем в общий список

user1 = User("Alice")
user2 = User("Bob")

# Случайно очищаем список
user1.all_users = []

print(f"User 1 users: {user1.all_users}")  # []
print(f"User 2 users: {user2.all_users}")  # [] <- Тоже пустой!
print(f"User class users: {User.all_users}")  # [<User Alice>, <User Bob>]

Вывод:

User 1 users: []
User 2 users: []  <- Странно!
User class users: [<User Alice>, <User Bob>]  <- Они здесь!

Правильный код:

class User:
    def __init__(self, name):
        self.name = name
    
    @classmethod
    def all_users(cls):
        """Использовать classmethod для доступа"""
        return [obj for obj in gc.get_objects() if isinstance(obj, cls)]
        # Или просто использовать класс переменную правильно

# Или с явным registry
class User:
    _registry = {}  # Приватная класс-переменная
    
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name
        User._registry[user_id] = self
    
    @classmethod
    def get_all(cls):
        return list(cls._registry.values())

user1 = User(1, "Alice")
user2 = User(2, "Bob")

print(User.get_all())  # [<User Alice>, <User Bob>]

Ключевые правила избежания ошибок изменяемости

  1. Никогда не используй изменяемые объекты как default параметры

    # ❌ Плохо
    def add_item(item, items=[]):
        items.append(item)
    
    # ✅ Хорошо
    def add_item(item, items=None):
        if items is None:
            items = []
        items.append(item)
    
  2. Копируй когда нужна независимость

    shallow_copy = original.copy()  # Для первого уровня
    deep_copy = copy.deepcopy(original)  # Для вложенных
    
  3. Используй immutable структуры когда возможно

    # Вместо списка
    config = {"debug": False}
    # Используй tuple когда не меняется
    point = (1, 2, 3)  # Вместо list
    
  4. Не мутируй в методах без явного намерения

    # ❌ Неожиданная мутация
    def process(items):
        items.sort()  # Кто это ожидает?
    
    # ✅ Явная мутация
    def sort_inplace(items):
        """Сортирует items на месте (mutating operation)"""
        items.sort()
    
    # ✅ Возвращает новый
    def get_sorted(items):
        return sorted(items)  # Новый список
    
  5. Избегай изменения списка во время итерации

    # ❌ Плохо
    for item in items:
        items.remove(item)
    
    # ✅ Хорошо
    items = [item for item in items if condition(item)]
    

Выводы

Ошибки изменяемости — это одни из самых коварных ошибок в Python, потому что:

  • Код выглядит логичным
  • Ошибка появляется только при определённых условиях
  • Очень сложно отлаживать (особенно в больших проектах)

Главное правило: Если ты не явно намерен мутировать объект, работай с копиями или immutable структурами. "Явное лучше неявного" — это золотое правило Python!

Приведи пример логической ошибки, из-за неучтенного фактора изменяемости | PrepBro