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

В чем особенность изменяемых данных?

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

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

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

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

# Изменяемые (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:

  1. Передаются по ссылке — изменения влияют на оригинал
  2. Проблема дефолтов — дефолтный параметр shared между вызовами
  3. Разница shallow vs deep copy — нужно копировать правильно
  4. Нельзя как ключи в dict — потеря целостности
  5. Race conditions в многопоточности — нужна синхронизация
  6. Производительность — создание нового объекта vs модификация

Понимание этого различия критично для написания правильного и безопасного Python кода.

В чем особенность изменяемых данных? | PrepBro