Комментарии (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 нужно осознанно выбирать, где использовать иммутабельные объекты, и учитывать баланс между безопасностью и производительностью.