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

Для чего нужны иммутабельные объекты?

1.7 Middle🔥 141 комментариев
#Python Core

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

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

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

Для чего нужны иммутабельные объекты

Иммутабельность — одна из ключевых концепций в программировании. Это означает, что объект не может быть изменён после создания.

Основные причины

1. Потокобезопасность (Thread Safety)

from threading import Thread

# ❌ Мутабельный список — проблемы с многопоточностью
shared_list = [1, 2, 3]

def modify_list():
    shared_list.append(4)  # Race condition!
    
t1 = Thread(target=modify_list)
t2 = Thread(target=modify_list)
t1.start()
t2.start()
print(shared_list)  # Результат непредсказуем

# ✅ Иммутабельный кортеж — безопасно
shared_tuple = (1, 2, 3)
# shared_tuple.append(4)  # TypeError: 'tuple' object has no attribute 'append'

2. Использование в качестве ключей словаря

# ❌ Мутабельный список нельзя использовать как ключ
my_dict = {}
my_list = [1, 2, 3]
my_dict[my_list] = "value"  # TypeError: unhashable type: 'list'

# ✅ Иммутабельный кортеж — идеально подходит
my_dict = {}
my_tuple = (1, 2, 3)
my_dict[my_tuple] = "value"  # OK!
print(my_dict)  # {(1, 2, 3): 'value'}

# Практический пример: кеширование координат
cache = {}
coord = (10, 20)
cache[coord] = "cached_value"

3. Создание множеств (Sets)

# ❌ Списки нельзя добавить в set
set_of_lists = {[1, 2], [3, 4]}  # TypeError: unhashable type: 'list'

# ✅ Кортежи работают
set_of_tuples = {(1, 2), (3, 4)}
print(set_of_tuples)  # {(1, 2), (3, 4)}

# Практический пример: уникальные координаты
visited_positions = {(0, 0), (1, 1), (2, 2)}
if (1, 1) in visited_positions:
    print("Already visited")

4. Кеширование и оптимизация

from functools import lru_cache

# ❌ Нельзя кешировать функции с мутабельными аргументами
# @lru_cache(maxsize=128)
# def process_data(data: list):
#     return sum(data)

# ✅ Кеширование с иммутабельными аргументами
@lru_cache(maxsize=128)
def process_data(data: tuple):
    return sum(data)

result1 = process_data((1, 2, 3))  # Вычисляет
result2 = process_data((1, 2, 3))  # Из кеша!

print(f"Cache info: {process_data.cache_info()}")
# Cache info: CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)

5. Предсказуемость и отладка

# ❌ Мутабельность создает проблемы
def modify_data(data):
    data[0] = 999  # Изменяем оригинальный объект!
    return data

original = [1, 2, 3]
result = modify_data(original)
print(original)  # [999, 2, 3] — шок!

# ✅ Иммутабельные объекты безопаснее
def process_data(data):
    new_data = (*data, 4)  # Создаём новый кортеж
    return new_data

original = (1, 2, 3)
result = process_data(original)
print(original)  # (1, 2, 3) — не изменился
print(result)    # (1, 2, 3, 4)

Иммутабельные типы данных в Python

# Встроенные иммутабельные типы:
immutable_int = 42  # int
immutable_str = "hello"  # str
immutable_tuple = (1, 2, 3)  # tuple
immutable_frozenset = frozenset({1, 2, 3})  # frozenset
immutable_bytes = b"data"  # bytes

# ✅ Создание иммутабельного класса
from dataclasses import dataclass

@dataclass(frozen=True)  # frozen=True делает класс иммутабельным
class Point:
    x: int
    y: int

p = Point(10, 20)
print(p)  # Point(x=10, y=20)
# p.x = 30  # FrozenInstanceError: cannot assign to field 'x'

# Можем использовать как ключ
point_cache = {p: "some_value"}

FrozenSet vs Set

# Мутабельное множество
mutable_set = {1, 2, 3}
mutable_set.add(4)  # OK
print(mutable_set)  # {1, 2, 3, 4}
# Нельзя использовать как ключ: {mutable_set: "val"}  # TypeError

# Иммутабельное множество
immutable_set = frozenset({1, 2, 3})
# immutable_set.add(4)  # AttributeError
print(immutable_set)  # frozenset({1, 2, 3})

# Можно использовать как ключ!
set_of_sets = {immutable_set: "value"}
print(set_of_sets)  # {frozenset({1, 2, 3}): 'value'}

Практический пример: граф с иммутабельными рёбрами

from typing import FrozenSet, Dict

class Graph:
    def __init__(self):
        # Используем frozenset для хранения рёбер (неупорядоченные пары)
        self.edges: FrozenSet[tuple] = frozenset()
        self.cache: Dict[frozenset, int] = {}  # Кеш результатов
    
    def add_edge(self, u: int, v: int):
        # Создаём новый frozenset вместо изменения старого
        edge = frozenset({u, v})
        self.edges = frozenset(self.edges | {edge})
    
    def has_edge(self, u: int, v: int) -> bool:
        edge = frozenset({u, v})
        return edge in self.edges

g = Graph()
g.add_edge(1, 2)
g.add_edge(2, 3)
print(g.has_edge(1, 2))  # True
print(g.has_edge(1, 3))  # False

Итоговые преимущества

  • Потокобезопасность: Нет race conditions
  • Хешируемость: Можно использовать как ключи
  • Кеширование: Поддержка @lru_cache
  • Предсказуемость: Функции не меняют входные данные
  • Производительность: Python может оптимизировать иммутабельные объекты
  • Функциональное программирование: Основа для паттернов вроде Copy-on-Write