Mutable и immutable: как изменяемость влияет на работу с данными в Python

Раздел: Типы данных -> Изменяемость типов

Изменяемые и неизменяемые типы в Python

В Python все данные представлены объектами, которые делятся на два основных класса по способности изменять своё содержимое после создания: изменяемые (mutable) и неизменяемые (immutable). Понимание этого различия необходимо для написания надёжного и предсказуемого кода, особенно при работе с функциями, копированием и хешированием.

Неизменяемые типы: int, float, str, tuple, frozenset, bytes. При любой операции, которая «изменяет» такой объект, на самом деле создаётся новый объект.

Изменяемые типы: list, dict, set, пользовательские классы (если не запрещено). Они позволяют менять своё содержимое без создания нового объекта.

# неизменяемый int
a = 10
b = a
a += 5  # создаётся новый объект 15, a ссылается на него
print(a, b)  # 15 10

Python изменяемые и неизменяемые типы (изменяемые и неизменяемые типы в python)

# изменяемый list
lst1 = [1, 2, 3]
lst2 = lst1
lst1.append(4)  # изменяется тот же объект
print(lst1, lst2)  # [1, 2, 3, 4] [1, 2, 3, 4]

Основное правило: если объект можно изменить без изменения его идентификатора (id), он изменяемый. Иначе – неизменяемый.

Как создать независимую копию изменяемого объекта, чтобы изменения в одной переменной не затрагивали другую?

Для списков и других изменяемых контейнеров используют поверхностное копирование через copy() или list(), а для глубокого – модуль copy.deepcopy().

import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
shallow[0].append(99)  # изменится и original[0]
print(original)  # [[1, 2, 99], [3, 4]]
print(deep)      # [[1, 2], [3, 4]] (не изменился)

Частая ошибка: использование оператора присваивания = для копирования изменяемых объектов. Это создаёт лишь новую ссылку, не копию. Для неизменяемых это безопасно, так как любые операции приводят к созданию нового объекта.

Как сделать изменяемый тип неизменяемым для использования в качестве ключа словаря или элемента множества?

Используют соответствующие неизменяемые «замороженные» версии: tuple вместо list, frozenset вместо set. Для преобразования списка в кортеж применяют tuple(my_list).

my_list = [10, 20, 30]
my_tuple = tuple(my_list)
d = {my_tuple: 'value'}  # работает
# d = {my_list: 'value'}  # TypeError: unhashable type: 'list'

Как проверить, изменился ли объект при операции?

Можно сравнивать id объекта до и после операции. Если идентификатор остался тем же – объект изменяемый, если изменился – неизменяемый.

s = 'hello'
print(id(s))
s += ' world'
print(id(s))  # другой id → строки неизменяемы

lst = [1, 2]
print(id(lst))
lst.append(3)
print(id(lst))  # тот же id → списки изменяемы

Ошибка: предположение, что += для строк – это изменение на месте. На самом деле это создание новой строки, что может быть неэффективно в цикле. Лучше собирать строки через .join().

Как избежать побочных эффектов при передаче изменяемого объекта в функцию?

Передавать копию объекта (через .copy() или deepcopy()), либо внутри функции работать с копией аргумента. Для неизменяемых объектов это не требуется – они передаются по значению (фактически по ссылке, но изменить их нельзя).

def add_item(container, item):
    container.append(item)  # меняет оригинал
    return container

my_list = [1, 2]
add_item(my_list.copy(), 3)  # безопасно: копия
print(my_list)  # [1, 2]

Дополнительные примеры по изменяемости типов

Пример
# Пример 1: хеширование и неизменяемость
# Неизменяемые объекты (int, str, tuple of immutables) могут быть ключами словаря.
valid_key = (1, 2, 3)
invalid_key = [1, 2, 3]
try:
    d = {invalid_key: 'error'}
except TypeError as e:
    print(e)  # unhashable type: 'list'

# Пример 2: кортеж с изменяемым элементом
mixed_tuple = (1, [2, 3])
# Сам кортеж неизменяем, но его элемент-список можно изменить.
mixed_tuple[1].append(4)
print(mixed_tuple)  # (1, [2, 3, 4])
# Хеш кортежа при этом не меняется, поэтому его всё равно нельзя использовать как ключ
# из-за наличия изменяемого элемента (будет ошибка при попытке создания словаря).
unhashable type: 'list'
(1, [2, 3, 4])
Пример
# Пример 3: влияние изменяемости на аргументы по умолчанию
# Использование изменяемого объекта по умолчанию опасно.
def append_to(element, target=[]):
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] (неожиданно)
# Решение: использовать None и создавать новый список внутри.
def append_to_fixed(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

print(append_to_fixed(1))  # [1]
print(append_to_fixed(2))  # [2]
[1]
[1, 2]
[1]
[2]
Пример
# Пример 4: сравнение по значению и идентификатору
# Для неизменяемых типов Python может оптимизировать память (интернирование).
a = 256
b = 256
print(a is b)  # True (малые целые кешируются)

c = 1000
d = 1000
print(c is d)  # False (обычно не кешируются)
print(c == d)  # True

# Для изменяемых is и == дают разный результат
x = [1, 2]
y = [1, 2]
print(x is y)  # False (разные объекты)
print(x == y)  # True (одинаковое содержимое)
True
False
True
False
True
Пример
# Пример 5: поведение срезов и операторов
# Для неизменяемых строк срез возвращает новую строку.
s = 'Python'
t = s[::-1]
print(t is s)  # False

# Для списков срез создаёт поверхностную копию
lst = [1, 2, 3]
copied = lst[:]
print(copied is lst)  # False
print(copied == lst)  # True
copied.append(4)
print(lst)  # [1, 2, 3] (оригинал не изменился)
False
False
True
[1, 2, 3]
Пример
# Пример 6: frozenset как неизменяемая альтернатива set
# Множества часто используют для проверки принадлежности, но set нельзя поместить в другой set.
set1 = {1, 2, 3}
frozen1 = frozenset([1, 2, 3])
# outer = {set1, frozen1}  # ошибка
outer = {frozen1, frozenset([4,5])}
print(outer)  # {frozenset({1, 2, 3}), frozenset({4, 5})}
{frozenset({1, 2, 3}), frozenset({4, 5})}

Изменяемые и неизменяемые типы в Python - comments

En
Python изменяемые и неизменяемые типы (python)