Что определяет иммутабельность класса в Python?

Ответ

Наличие атрибутов в классе не делает его автоматически иммутабельным. Иммутабельность (неизменяемость) объекта в Python определяется тем, можно ли изменить его состояние (значения его атрибутов) после создания.

Почему иммутабельность важна:

  • Предсказуемость: Состояние объекта не меняется, что упрощает отладку и понимание кода.
  • Потокобезопасность: Иммутабельные объекты по своей природе потокобезопасны, так как их не нужно защищать от одновременного изменения разными потоками.
  • Использование в качестве ключей: Иммутабельные объекты могут быть использованы как ключи в словарях (dict) или элементы в множествах (set), поскольку их хеш-значение не меняется.

Как добиться иммутабельности в Python:

  1. Запрет изменения атрибутов:

    • Переопределение метода __setattr__ для запрета присваивания значений атрибутам после инициализации.
    • Использование __slots__ для предотвращения динамического добавления новых атрибутов и оптимизации памяти.
  2. Использование dataclasses с frozen=True (Python 3.7+): Это наиболее современный и простой способ создания иммутабельных классов.

Пример изменяемого (Mutable) класса:

class MutablePoint:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

point = MutablePoint(1, 2)
print(f"Исходная точка: ({point.x}, {point.y})") # Вывод: (1, 2)
point.x = 10 # Атрибут можно изменить
print(f"Измененная точка: ({point.x}, {point.y})") # Вывод: (10, 2)

Пример иммутабельного (Immutable) класса (ручная реализация):

class ImmutablePoint:
    __slots__ = ('_x', '_y') # Используем __slots__ и префикс '_' для внутренних атрибутов

    def __init__(self, x: int, y: int):
        # Для инициализации атрибутов в иммутабельном классе нужно обойти __setattr__
        # или использовать super().__setattr__
        super().__setattr__('_x', x)
        super().__setattr__('_y', y)

    @property
    def x(self) -> int:
        return self._x

    @property
    def y(self) -> int:
        return self._y

    def __setattr__(self, name, value):
        # Запрещаем изменение любых атрибутов после инициализации
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}' or is immutable")

    def __repr__(self):
        return f"ImmutablePoint(x={self.x}, y={self.y})"

point = ImmutablePoint(1, 2)
print(f"Исходная точка: {point}") # Вывод: ImmutablePoint(x=1, y=2)
try:
    point.x = 10 # Попытка изменить атрибут вызовет ошибку
except AttributeError as e:
    print(f"Ошибка при попытке изменения: {e}") # Вывод: Ошибка при попытке изменения: 'ImmutablePoint' object has no attribute 'x' or is immutable

Пример иммутабельного класса с dataclasses (рекомендуемый способ):

from dataclasses import dataclass

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

point = FrozenPoint(1, 2)
print(f"Исходная точка: {point}") # Вывод: FrozenPoint(x=1, y=2)
try:
    point.x = 10 # Попытка изменить атрибут вызовет ошибку
except Exception as e: # dataclasses.FrozenInstanceError
    print(f"Ошибка при попытке изменения: {type(e).__name__}: {e}") # Вывод: FrozenInstanceError: cannot assign to field 'x'

Таким образом, иммутабельность класса в Python достигается не просто наличием атрибутов, а строгим контролем над их изменением после создания объекта.