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 10Python изменяемые и неизменяемые типы (изменяемые и неизменяемые типы в 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})}