Приведи пример логической ошибки, из-за неучтенного фактора изменяемости
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Логические ошибки изменяемости (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>]
Ключевые правила избежания ошибок изменяемости
-
Никогда не используй изменяемые объекты как default параметры
# ❌ Плохо def add_item(item, items=[]): items.append(item) # ✅ Хорошо def add_item(item, items=None): if items is None: items = [] items.append(item) -
Копируй когда нужна независимость
shallow_copy = original.copy() # Для первого уровня deep_copy = copy.deepcopy(original) # Для вложенных -
Используй immutable структуры когда возможно
# Вместо списка config = {"debug": False} # Используй tuple когда не меняется point = (1, 2, 3) # Вместо list -
Не мутируй в методах без явного намерения
# ❌ Неожиданная мутация def process(items): items.sort() # Кто это ожидает? # ✅ Явная мутация def sort_inplace(items): """Сортирует items на месте (mutating operation)""" items.sort() # ✅ Возвращает новый def get_sorted(items): return sorted(items) # Новый список -
Избегай изменения списка во время итерации
# ❌ Плохо for item in items: items.remove(item) # ✅ Хорошо items = [item for item in items if condition(item)]
Выводы
Ошибки изменяемости — это одни из самых коварных ошибок в Python, потому что:
- Код выглядит логичным
- Ошибка появляется только при определённых условиях
- Очень сложно отлаживать (особенно в больших проектах)
Главное правило: Если ты не явно намерен мутировать объект, работай с копиями или immutable структурами. "Явное лучше неявного" — это золотое правило Python!