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

Что такое неизменяемые структуры данных?

2.0 Middle🔥 121 комментариев
#Другое

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

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

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

Immutable Data Structures — Неизменяемые структуры данных

Неизменяемые (Immutable) структуры данных — это объекты, которые не могут быть изменены после создания. Вместо модификации исходного объекта, операции возвращают новый объект с изменёнными данными. Это фундаментальный принцип функционального программирования, обеспечивающий безопасность потоков и предсказуемость кода.

Встроенные неизменяемые типы Python

Python имеет несколько встроенных неизменяемых типов:

# 1. Строки (immutable)
s = 'Hello'
# s[0] = 'J'  # TypeError: 'str' object does not support item assignment
new_s = s.replace('H', 'J')  # Создаёт новую строку
print(new_s)  # 'Jello'

# 2. Кортежи (immutable)
t = (1, 2, 3)
# t[0] = 10  # TypeError: 'tuple' object does not support item assignment
new_t = t + (4, 5)  # Создаёт новый кортеж
print(new_t)  # (1, 2, 3, 4, 5)

# 3. Числа (immutable)
x = 42
# x[0] = 5  # Невозможно (числа — скаляры)
new_x = x + 1  # Создаёт новое число
print(new_x)  # 43

# 4. frozenset (неизменяемое множество)
fs = frozenset([1, 2, 3])
# fs.add(4)  # AttributeError: 'frozenset' object has no attribute 'add'
new_fs = fs | frozenset([4, 5])  # Создаёт новое множество
print(new_fs)  # frozenset({1, 2, 3, 4, 5})

# Изменяемые типы (для сравнения)
# Список (mutable)
lst = [1, 2, 3]
lst[0] = 10  # Изменяет список на месте
print(lst)  # [10, 2, 3]

# Словарь (mutable)
d = {'a': 1}
d['a'] = 2  # Изменяет словарь на месте
print(d)  # {'a': 2}

Создание собственных неизменяемых классов

from dataclasses import dataclass
from typing import NamedTuple

# Способ 1: dataclass с frozen=True
@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)
# p.x = 3.0  # FrozenInstanceError: cannot assign to field 'x'

# Способ 2: NamedTuple
class Location(NamedTuple):
    latitude: float
    longitude: float

loc = Location(37.7749, -122.4194)
print(loc)  # Location(latitude=37.7749, longitude=-122.4194)
# loc.latitude = 37.0  # AttributeError: can't set attribute

# Способ 3: Пользовательский класс с __slots__
class Immutable:
    __slots__ = ('_x', '_y')  # Запретить добавление новых атрибутов
    
    def __init__(self, x: int, y: int):
        object.__setattr__(self, '_x', x)
        object.__setattr__(self, '_y', y)
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    def __setattr__(self, name, value):
        raise AttributeError('Cannot modify immutable object')

im = Immutable(10, 20)
print(im.x, im.y)  # 10 20
# im.x = 30  # AttributeError: Cannot modify immutable object

Преимущества неизменяемости

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

from threading import Thread
from dataclasses import dataclass

@dataclass(frozen=True)
class ImmutableUser:
    id: int
    name: str

# Безопасно использовать в разных потоках без мьютекса
user = ImmutableUser(1, 'Alice')

def thread_func():
    print(user.name)  # Никогда не изменится

threads = [Thread(target=thread_func) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

2. Кэширование и хеширование

from dataclasses import dataclass

@dataclass(frozen=True)
class UserKey:
    id: int
    email: str

# Можно использовать как ключ словаря (нужна хешируемость)
cache = {}
user_key = UserKey(1, 'alice@example.com')
cache[user_key] = {'name': 'Alice', 'age': 25}

# Повторное использование того же ключа
print(cache[user_key])  # {'name': 'Alice', 'age': 25}

# Это не работает с изменяемыми объектами
mutable_dict = {'id': 1, 'email': 'alice@example.com'}
# cache[mutable_dict] = ...  # TypeError: unhashable type: 'dict'

3. Предсказуемость и отсутствие побочных эффектов

from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    amount: float
    currency: str

def add_tax(money: Money, tax_rate: float) -> Money:
    """Функция НЕ изменяет исходный объект"""
    new_amount = money.amount * (1 + tax_rate)
    return Money(new_amount, money.currency)

original = Money(100, 'USD')
with_tax = add_tax(original, 0.1)

print(original)   # Money(amount=100, currency='USD')  Не изменился!
print(with_tax)   # Money(amount=110, currency='USD')  Новый объект

Практический пример: неизменяемое состояние приложения

from dataclasses import dataclass
from typing import List, Tuple
from datetime import datetime

@dataclass(frozen=True)
class Task:
    id: int
    title: str
    completed: bool

@dataclass(frozen=True)
class AppState:
    tasks: Tuple[Task, ...]  # Используем кортеж (immutable)
    selected_task_id: int
    last_update: datetime

# Функция для добавления задачи (возвращает новое состояние)
def add_task(state: AppState, task: Task) -> AppState:
    new_tasks = state.tasks + (task,)  # Создаём новый кортеж
    return AppState(
        tasks=new_tasks,
        selected_task_id=state.selected_task_id,
        last_update=datetime.now()
    )

# Функция для завершения задачи
def complete_task(state: AppState, task_id: int) -> AppState:
    new_tasks = tuple(
        Task(t.id, t.title, True) if t.id == task_id else t
        for t in state.tasks
    )
    return AppState(
        tasks=new_tasks,
        selected_task_id=state.selected_task_id,
        last_update=datetime.now()
    )

# Использование
initial_state = AppState(
    tasks=(),
    selected_task_id=0,
    last_update=datetime.now()
)

state1 = add_task(initial_state, Task(1, 'Learn Python', False))
state2 = add_task(state1, Task(2, 'Learn FastAPI', False))
state3 = complete_task(state2, 1)

print(f'State 1: {len(state1.tasks)} tasks')  # 1 task
print(f'State 2: {len(state2.tasks)} tasks')  # 2 tasks
print(f'State 3: {len(state3.tasks)} tasks')  # 2 tasks
print(f'Task 1 in state3: {state3.tasks[0].completed}')  # True

Неизменяемость в SQLAlchemy

from sqlalchemy import Column, Integer, String, create_engine, event
from sqlalchemy.orm import declarative_base, Session
from dataclasses import dataclass

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    email = Column(String(100))

# Снимок (snapshot) для отслеживания изменений
@dataclass(frozen=True)
class UserSnapshot:
    id: int
    name: str
    email: str

def entity_to_snapshot(user: User) -> UserSnapshot:
    """Преобразуем изменяемую сущность в неизменяемый снимок"""
    return UserSnapshot(user.id, user.name, user.email)

# Использование
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)

with Session(engine) as session:
    user = User(id=1, name='Alice', email='alice@example.com')
    session.add(user)
    session.commit()
    
    # Делаем снимок
    snapshot = entity_to_snapshot(user)
    print(snapshot)  # UserSnapshot(id=1, name='Alice', email='alice@example.com')
    
    # Даже если пользователь изменится в БД, снимок остаётся неизменным
    user.name = 'Alice Smith'
    print(snapshot.name)  # 'Alice' (не изменился)

Проблемы с неизменяемостью

1. Производительность памяти

# Создание много новых объектов — потребляет память
data = tuple(range(1000000))
# Каждая операция создаёт новый кортеж
new_data = data + (1000000,)  # Копирует весь кортеж!

# Лучше использовать список, а потом преобразовать
data_list = list(range(1000000))
data_list.append(1000000)
final_data = tuple(data_list)

2. Сложность для вложенных структур

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Address:
    street: str
    city: str

@dataclass(frozen=True)
class User:
    name: str
    address: Address

user = User('Alice', Address('123 Main St', 'NYC'))
# Хотим изменить город — нужно создать новую структуру целиком!
new_user = User(user.name, Address(user.address.street, 'LA'))

# Лучше использовать dataclass.replace() (Python 3.10+)
new_user = replace(user, address=replace(user.address, city='LA'))

Использование с функциональным программированием

from typing import List
from dataclasses import dataclass

@dataclass(frozen=True)
class Item:
    id: int
    price: float

def filter_expensive(items: List[Item], min_price: float) -> List[Item]:
    """Чистая функция (без побочных эффектов)"""
    return [item for item in items if item.price >= min_price]

def apply_discount(items: List[Item], discount: float) -> List[Item]:
    """Чистая функция (создаёт новые объекты)"""
    return [
        Item(item.id, item.price * (1 - discount))
        for item in items
    ]

# Использование
original_items = [
    Item(1, 100),
    Item(2, 200),
    Item(3, 150)
]

expensive = filter_expensive(original_items, 150)
discounted = apply_discount(expensive, 0.1)

print(original_items)  # Не изменился
print(expensive)       # Отфильтрованные товары
print(discounted)      # С скидкой

Резюме: Неизменяемые структуры данных — это объекты, которые не изменяются после создания. Вместо модификации операции возвращают новые объекты. Это обеспечивает:

  • Потокобезопасность (можно использовать в разных потоках без синхронизации)
  • Предсказуемость (функции не имеют побочных эффектов)
  • Кэшируемость (можно хешировать и использовать как ключи)
  • Отслеживание истории (каждое состояние сохраняется)

Хотя неизменяемость требует больше памяти, она критична для надёжных, масштабируемых приложений.