Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Изменяемые (Mutable) данные в Python
Изменяемость — это один из самых важных концептов в Python, который влияет на практически всё: от производительности до сложных багов. Расскажу подробно.
Определение
Изменяемые данные (mutable) — это объекты, которые можно модифицировать после создания, не создавая новый объект.
Неизменяемые данные (immutable) — это объекты, которые нельзя модифицировать. При изменении создаётся новый объект.
Основные типы
Изменяемые (Mutable)
# List — самый распространённый
my_list = [1, 2, 3]
my_list[0] = 999 # Модифицируем
print(my_list) # [999, 2, 3] — ТОТ ЖЕ объект
my_list.append(4) # Добавляем элемент
print(my_list) # [999, 2, 3, 4]
# Dict
my_dict = {'name': 'John'}
my_dict['age'] = 30 # Добавляем ключ
print(my_dict) # {'name': 'John', 'age': 30}
# Set
my_set = {1, 2, 3}
my_set.add(4) # Добавляем элемент
print(my_set) # {1, 2, 3, 4}
# bytearray
my_bytes = bytearray(b'hello')
my_bytes[0] = ord('H') # Меняем байт
print(my_bytes) # bytearray(b'Hello')
Неизменяемые (Immutable)
# String
my_string = "hello"
# my_string[0] = 'H' # TypeError: 'str' object does not support item assignment
new_string = my_string.upper() # Создаётся НОВЫЙ объект
print(my_string) # "hello" — не изменилась
print(new_string) # "HELLO" — новый объект
# Tuple
my_tuple = (1, 2, 3)
# my_tuple[0] = 999 # TypeError: 'tuple' object does not support item assignment
# Можно создать новый tuple, но не модифицировать существующий
new_tuple = (999,) + my_tuple[1:] # (999, 2, 3)
# frozenset
my_frozenset = frozenset([1, 2, 3])
# my_frozenset.add(4) # AttributeError: 'frozenset' object has no attribute 'add'
# int, float, bool
x = 5
# x[0] = 1 # Нет смысла, это примитив
y = x + 1 # Создаётся НОВЫЙ объект, x остаётся 5
Разница в памяти
Это очень важно для производительности:
import sys
# Неизменяемое — новый объект при "изменении"
s1 = "hello"
s2 = s1.upper() # Создаётся новый объект
print(id(s1)) # 140234567890
print(id(s2)) # 140234567920 ← РАЗНЫЕ объекты
# Изменяемое — тот же объект
list1 = [1, 2, 3]
list2 = list1
list2.append(4) # Модифицируем
print(list1) # [1, 2, 3, 4] ← ИЗМЕНИЛАСЬ!
print(id(list1)) # 140234567890
print(id(list2)) # 140234567890 ← ОДИНАКОВЫЕ
Главные особенности изменяемых данных
1. Передача по ссылке
Это самая опасная особенность:
def modify_list(lst):
lst.append(999) # Модифицируем исходный список
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # [1, 2, 3, 999] ← ИЗМЕНИЛАСЬ!
Выход: использовать копию
def modify_list(lst):
lst = lst.copy() # Или lst[:]
lst.append(999)
return lst
my_list = [1, 2, 3]
result = modify_list(my_list)
print(my_list) # [1, 2, 3] ← не изменилась
print(result) # [1, 2, 3, 999]
2. Проблема с дефолтными параметрами
Это частый источник ошибок:
# ❌ ПЛОХО
def append_item(item, lst=[]):
lst.append(item)
return lst
result1 = append_item(1) # [1]
result2 = append_item(2) # [1, 2] ← что?!
result3 = append_item(3) # [1, 2, 3] ← ещё хуже!
print(result1) # [1, 2, 3] ← все указывают на ОДИН объект
print(result2) # [1, 2, 3]
print(result3) # [1, 2, 3]
Почему? Дефолтный параметр создаётся один раз, при определении функции, и переиспользуется:
def append_item(item, lst=[]):
print(id(lst)) # Всегда один и тот же id!
lst.append(item)
return lst
append_item(1)
append_item(2)
append_item(3)
# Output:
# 140234567890
# 140234567890 ← одинаковый!
# 140234567890
✅ Правильно:
def append_item(item, lst=None):
if lst is None:
lst = [] # Новый объект каждый раз
lst.append(item)
return lst
result1 = append_item(1) # [1]
result2 = append_item(2) # [2] ← отдельно
result3 = append_item(3) # [3] ← отдельно
3. Неглубокое копирование vs Глубокое копирование
# Неглубокое копирование (shallow copy)
original = [[1, 2], [3, 4]]
copy_shallow = original.copy() # Или original[:]
copy_shallow[0][0] = 999
print(original) # [[999, 2], [3, 4]] ← внутренний список изменился!
# Глубокое копирование (deep copy)
import copy
copy_deep = copy.deepcopy(original)
copy_deep[0][0] = 999
print(original) # [[1, 2], [3, 4]] ← не изменилась
Почему?
Shallow copy создаёт новый список, но элементы указывают на те же объекты:
original: [object1] ──┐
[object2] ┌┘
↓
copy_shallow: [object1] ──┐ ← НОВЫЙ список
[object2] ┌┘ ← но те же внутренние объекты
Do we modify object1:
original[0] → object1 → изменится
copy_shallow[0] → object1 → также видит изменение
Do we modify сам внешний список:
original.append(...) ← не видит copy_shallow
copy_shallow.append(...) ← не видит original
4. Проблема в многопоточности
import threading
counter = {'count': 0}
def increment():
for _ in range(1000000):
counter['count'] += 1 # Race condition!
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(counter['count']) # Expected: 2000000
# Actual: 1234567 (random!)
Почему? Два потока одновременно читают и пишут в dict:
Поток 1 Поток 2
read: 500 read: 500
write: 501 write: 501 ← обе пишут 501!
Решение: использовать Lock
import threading
counter = {'count': 0}
lock = threading.Lock()
def increment():
for _ in range(1000000):
with lock:
counter['count'] += 1
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(counter['count']) # 2000000 — правильно
5. Нельзя использовать как ключ в dict
# ✅ Неизменяемое — можно
my_dict = {}
my_dict[(1, 2, 3)] = 'tuple key' # tuple — ключ
print(my_dict) # {(1, 2, 3): 'tuple key'}
# ❌ Изменяемое — нельзя
my_dict = {}
my_dict[[1, 2, 3]] = 'list key' # TypeError: unhashable type: 'list'
# Потому что после добавления в dict, если список изменится,
# хеш функция даст другой результат, и dict "потеряет" ключ
Таблица: Изменяемые vs Неизменяемые
| Тип | Изменяемость | Хешируемость | Используется в dict | Использование |
|---|---|---|---|---|
list | Изменяемая | Нет | Нет | Коллекции данных |
dict | Изменяемая | Нет | Нет | Ключ-значение |
set | Изменяемая | Нет | Нет | Уникальные элементы |
tuple | Неизменяемая | Да | Да | Составные ключи |
frozenset | Неизменяемая | Да | Да | Ключи в dict |
str | Неизменяемая | Да | Да | Текст |
int, float, bool | Неизменяемая | Да | Да | Примитивы |
Практические советы
1. Всегда проверяй, передаёшь ли ссылку
# ❌ Плохо — передаёт ссылку
def process_data(data):
data['status'] = 'processed'
return data
# ✅ Хорошо — создаёт новый объект
def process_data(data):
result = data.copy()
result['status'] = 'processed'
return result
# ✅ Хорошо — делает новый объект явно
def process_data(data):
return {**data, 'status': 'processed'}
2. Избегай изменяемых дефолтов
# ❌ Плохо
def create_user(name, tags=[]):
tags.append('user')
return {'name': name, 'tags': tags}
# ✅ Хорошо
def create_user(name, tags=None):
if tags is None:
tags = []
tags.append('user')
return {'name': name, 'tags': tags}
# ✅ Ещё лучше — не мутировать параметр
def create_user(name, tags=None):
if tags is None:
tags = []
return {'name': name, 'tags': tags + ['user']}
3. Используй tuple вместо list, если нет необходимости менять
# ❌ Список — оверкилл если не меняется
DEFAULT_COLORS = ['red', 'green', 'blue']
# ✅ Tuple — правильно
DEFAULT_COLORS = ('red', 'green', 'blue')
# Преимущества:
# 1. Быстрее в создании и хранении
# 2. Можно использовать как ключ dict
# 3. Явно показывает, что не меняется
4. Deep copy для сложных структур
import copy
# Если есть вложенные структуры
original = {
'users': [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'}
]
}
# Shallow copy недостаточно
simple_copy = original.copy()
simple_copy['users'][0]['name'] = 'Charlie'
print(original['users'][0]['name']) # 'Charlie' ← повлияло на оригинал!
# Нужен deep copy
deep_copy = copy.deepcopy(original)
deep_copy['users'][0]['name'] = 'Charlie'
print(original['users'][0]['name']) # 'Alice' ← не повлияло
Резюме
Особенности изменяемых данных в Python:
- Передаются по ссылке — изменения влияют на оригинал
- Проблема дефолтов — дефолтный параметр shared между вызовами
- Разница shallow vs deep copy — нужно копировать правильно
- Нельзя как ключи в dict — потеря целостности
- Race conditions в многопоточности — нужна синхронизация
- Производительность — создание нового объекта vs модификация
Понимание этого различия критично для написания правильного и безопасного Python кода.