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

Что такое иммутабельность?

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

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

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

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

Иммутабельность (Immutability)

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

Основная идея

Объект называется иммутабельным, если:

  • Его состояние не может быть изменено после инициализации
  • Попытка изменения создаёт новый объект вместо модификации существующего
  • Это безопасно для использования в многопоточной среде без синхронизации

Встроенные иммутабельные типы в Python

# Иммутабельные типы
immutable_int = 42
immutable_str = "hello"
immutable_tuple = (1, 2, 3)
immutable_frozenset = frozenset([1, 2, 3])
immutable_bytes = b"hello"

# Попытка изменить иммутабельный объект создаёт новый объект
s = "hello"
print(id(s))  # 140234567891200
s = s + " world"
print(id(s))  # 140234567891234 - другой объект!

# Мутабельные типы
mutable_list = [1, 2, 3]
original_id = id(mutable_list)
mutable_list.append(4)  # Изменяет существующий объект
print(id(mutable_list))  # Тот же ID!

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

1. Использование @dataclass с frozen=True

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    """Иммутабельный класс точки"""
    x: float
    y: float
    z: float = 0.0
    
    def distance_from_origin(self) -> float:
        return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5

# Создание объекта
point = Point(3, 4, 5)
print(point.distance_from_origin())  # 7.071...

# Попытка изменить — ошибка
try:
    point.x = 10
except Exception as e:
    print(f"Error: {e}")  # Error: cannot assign to field 'x'

2. Ручная реализация с slots и setattr

class ImmutableUser:
    """Иммутабельный класс пользователя"""
    __slots__ = ('_name', '_email', '_age')
    
    def __init__(self, name: str, email: str, age: int):
        object.__setattr__(self, '_name', name)
        object.__setattr__(self, '_email', email)
        object.__setattr__(self, '_age', age)
    
    def __setattr__(self, name, value):
        raise AttributeError("Cannot modify immutable object")
    
    @property
    def name(self) -> str:
        return self._name
    
    @property
    def email(self) -> str:
        return self._email
    
    @property
    def age(self) -> int:
        return self._age
    
    def __repr__(self) -> str:
        return f"ImmutableUser(name={self._name!r}, email={self._email!r}, age={self._age})"

# Использование
user = ImmutableUser("Alice", "alice@example.com", 30)
print(user)  # ImmutableUser(name='Alice', email='alice@example.com', age=30)

# Попытка изменить
try:
    user.name = "Bob"
except AttributeError as e:
    print(f"Error: {e}")  # Error: Cannot modify immutable object

3. Использование NamedTuple

from typing import NamedTuple

class Product(NamedTuple):
    """Иммутабельный класс продукта"""
    id: int
    name: str
    price: float
    stock: int = 0
    
    def apply_discount(self, discount_percent: float):
        """Вернуть новый объект с скидкой"""
        new_price = self.price * (1 - discount_percent / 100)
        return Product(
            id=self.id,
            name=self.name,
            price=new_price,
            stock=self.stock
        )

# Использование
product = Product(1, "Laptop", 999.99, 10)
print(product)  # Product(id=1, name='Laptop', price=999.99, stock=10)

# Применить скидку (создаёт новый объект)
discounted = product.apply_discount(10)
print(discounted)  # Product(id=1, name='Laptop', price=899.991, stock=10)
print(product)  # Исходный объект не изменился

Практические примеры

Иммутабельный конфиг

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class AppConfig:
    """Иммутабельная конфигурация приложения"""
    debug: bool
    database_url: str
    api_key: str
    timeout: int = 30
    max_retries: int = 3
    
    def with_debug(self, debug: bool):
        """Создать новую конфигурацию с изменённым debug"""
        return AppConfig(
            debug=debug,
            database_url=self.database_url,
            api_key=self.api_key,
            timeout=self.timeout,
            max_retries=self.max_retries
        )

# Использование
config = AppConfig(
    debug=False,
    database_url="postgresql://localhost/mydb",
    api_key="secret-key-123"
)

print(config)  # AppConfig(debug=False, database_url='postgresql://localhost/mydb', api_key='secret-key-123', timeout=30, max_retries=3)

# Создать новую конфигурацию с debug=True
debug_config = config.with_debug(True)
print(debug_config.debug)  # True
print(config.debug)  # False - исходная не изменилась

Иммутабельное состояние в приложении

from dataclasses import dataclass, replace
from typing import List
from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

@dataclass(frozen=True)
class Order:
    """Иммутабельный заказ"""
    id: str
    customer_id: str
    status: OrderStatus
    total_amount: float
    items: tuple  # Используем tuple вместо list
    created_at: str
    updated_at: str
    
    def mark_as_paid(self, payment_id: str):
        """Создать новый заказ со статусом PAID"""
        if self.status != OrderStatus.PENDING:
            raise ValueError(f"Cannot pay order in {self.status} status")
        
        return replace(self, status=OrderStatus.PAID)
    
    def ship(self, tracking_number: str):
        """Создать новый заказ со статусом SHIPPED"""
        if self.status != OrderStatus.PAID:
            raise ValueError(f"Cannot ship order in {self.status} status")
        
        return replace(self, status=OrderStatus.SHIPPED)

# Использование
order = Order(
    id="ORD-001",
    customer_id="CUST-001",
    status=OrderStatus.PENDING,
    total_amount=99.99,
    items=("item1", "item2"),
    created_at="2024-01-01T10:00:00",
    updated_at="2024-01-01T10:00:00"
)

print(f"Original: {order.status}")  # PENDING

# Переходы состояния создают новые объекты
paid_order = order.mark_as_paid("PAY-123")
print(f"After payment: {paid_order.status}")  # PAID
print(f"Original still: {order.status}")  # PENDING

shipped_order = paid_order.ship("TRACK-123")
print(f"After shipping: {shipped_order.status}")  # SHIPPED

Кэширование с иммутабельностью

from functools import lru_cache
from dataclasses import dataclass

@dataclass(frozen=True)
class CacheKey:
    """Иммутабельный ключ кэша"""
    user_id: int
    period: str

class UserAnalytics:
    @lru_cache(maxsize=128)
    def get_user_stats(self, cache_key: CacheKey):
        """Получить статистику (результат кэшируется)"""
        print(f"Computing stats for user {cache_key.user_id}")
        return {
            "page_views": 1000,
            "period": cache_key.period
        }

# Использование
analytics = UserAnalytics()

# Первый вызов — вычисляется
key1 = CacheKey(user_id=1, period="2024-01")
stats1 = analytics.get_user_stats(key1)
print(stats1)  # Computing stats for user 1, затем выводит результат

# Второй вызов с тем же ключом — из кэша
key2 = CacheKey(user_id=1, period="2024-01")
stats2 = analytics.get_user_stats(key2)
print(stats2)  # Выводит результат (не печатает "Computing...")

# Другой ключ — новое вычисление
key3 = CacheKey(user_id=2, period="2024-01")
stats3 = analytics.get_user_stats(key3)
print(stats3)  # Computing stats for user 2

Преимущества иммутабельности

# 1. Безопасность в многопоточной среде
import threading

@dataclass(frozen=True)
class ThreadSafeConfig:
    value: str

config = ThreadSafeConfig("shared")

def thread_worker():
    # Без блокировок — объект не может быть изменён
    print(config.value)

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

# 2. Коррекность рассуждений
def process_data(data: tuple):
    """Можно полагаться, что data не изменится во время выполнения"""
    for item in data:
        print(item)

data = (1, 2, 3)
process_data(data)  # Гарантированно безопасно

# 3. Простота тестирования
@dataclass(frozen=True)
class TestableClass:
    x: int
    
    def double(self):
        return TestableClass(x=self.x * 2)

def test_double():
    obj = TestableClass(x=5)
    result = obj.double()
    assert result.x == 10
    assert obj.x == 5  # Исходный не изменился

Недостатки иммутабельности

# Недостаток 1: Частое создание новых объектов
@dataclass(frozen=True)
class MutableAlternative:
    data: list  # Плохо — data может содержать мутабельные объекты

# Недостаток 2: Производительность
# Создание новых объектов — дороже, чем изменение существующего

# Недостаток 3: Синтаксическая громоздкость
config = AppConfig(...)
new_config = config.with_debug(True)  # Многословно

Best Practices

# 1. Использовать frozen dataclasses
@dataclass(frozen=True)
class Good:
    x: int
    y: int

# 2. Избегать мутабельных типов в иммутабельных объектах
@dataclass(frozen=True)
class GoodWithTuple:
    items: tuple  # Используем tuple

# Плохо:
@dataclass(frozen=True)
class BadWithList:
    items: list  # Плохо! Список мутабельный

# 3. Использовать методы для создания модифицированных копий
@dataclass(frozen=True)
class ConfigGood:
    debug: bool
    
    def with_debug(self, debug: bool):
        return ConfigGood(debug=debug)

# 4. Использовать replace() для удобства
from dataclasses import replace

config = ConfigGood(debug=False)
new_config = replace(config, debug=True)

Итоги

Иммутабельность — мощная концепция для:

  • Безопасного многопоточного программирования
  • Предсказуемого поведения программы
  • Упрощения рассуждений о коде
  • Кэширования и оптимизации
  • Функционального стиля программирования

В Python нужно осознанно выбирать, где использовать иммутабельные объекты, и учитывать баланс между безопасностью и производительностью.