Как создать неизменяемый (immutable) класс в Python

Ответ

Неизменяемый (immutable) объект — это объект, состояние которого нельзя изменить после его создания. Это повышает предсказуемость и безопасность кода, особенно в многопоточных приложениях, а также позволяет использовать объекты в качестве ключей словаря.

В Python есть несколько способов создать неизменяемый класс:

1. @dataclass(frozen=True) (Python 3.7+)

Это самый современный и предпочтительный способ. Декоратор dataclass с параметром frozen=True автоматически делает класс неизменяемым.

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(10, 20)
print(p.x)  # 10

try:
    p.x = 30  # Попытка изменить атрибут
except FrozenInstanceError as e:
    print(e)  # Вывод: cannot assign to field 'x'

2. collections.namedtuple

Это фабричная функция для создания простых классов-кортежей. Объекты namedtuple легковесны и неизменяемы по своей природе.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

p = Point(10, 20)
print(p.x) # 10

# Попытка изменения вызовет AttributeError
# p.x = 30 -> AttributeError: can't set attribute

3. Ручная реализация через __slots__ и __setattr__

Этот способ дает полный контроль, но является наиболее многословным. Он полезен для понимания механики неизменяемости.

class ImmutablePoint:
    __slots__ = ('x', 'y') # Экономит память и предотвращает создание __dict__

    def __init__(self, x, y):
        # Используем object.__setattr__ для инициализации полей
        super().__setattr__('x', x)
        super().__setattr__('y', y)

    def __setattr__(self, name, value):
        # Переопределяем метод, чтобы запретить изменения
        raise AttributeError(f"Can't modify attribute {name}")

Ответ 18+ 🔞

Да ты послушай, что за дичь творится с этими объектами! Вот сидишь ты, пишешь код, а он тебе в какой-то момент — раз! — и меняется под носом, как последняя шлюха. А кто поменял? А хуй его знает! Потоков дохуя, все друг другу мозги ебут. Надо это прекращать.

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

В Python, слава богу, есть несколько способов эту святость обеспечить. Смотри сюда, не зевай.

1. @dataclass(frozen=True) (Python 3.7+)

Это, блядь, самый модный и правильный способ сейчас. Надел на класс этот декоратор, сказал frozen=True — и всё, пиздец. Автоматом всё замораживается, как твои яйца в феврале.

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(10, 20)
print(p.x)  # 10, всё чётко

try:
    p.x = 30  # Пробуем наебать систему
except FrozenInstanceError as e:
    print(e)  # И получаем по ебалу: cannot assign to field 'x'

Вот и весь сказ. Попробуй изменить — получи FrozenInstanceError прямо в ебальник.

2. collections.namedtuple

Старая, добрая, консервативная штука. Лёгкая, как пух, и неизменяемая от рождения, как пидарас шерстяной. Фабрика по производству кортежей с именами.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

p = Point(10, 20)
print(p.x) # 10, красота

# А теперь попробуй тронь, сука
# p.x = 30 -> AttributeError: can't set attribute
# Вот тебе и вся недолга.

Просто, быстро, без наворотов. Для простых случаев — то, что доктор прописал.

3. Ручная возня с __slots__ и __setattr__

А это, дружок, для истинных мазохистов и ценителей старой школы. Полный контроль, но и писать надо, овердохуища. Зато поймёшь, как эта магия изнутри работает.

class ImmutablePoint:
    __slots__ = ('x', 'y') # Экономит память, да и __dict__ не создаёт, чтоб не маялся

    def __init__(self, x, y):
        # Тут надо хитрить, как лиса. Используем родной __setattr__ от object.
        super().__setattr__('x', x)
        super().__setattr__('y', y)

    def __setattr__(self, name, value):
        # А это — священный щит. Любая попытка изменения — получай ошибку в рыло.
        raise AttributeError(f"Can't modify attribute {name}, иди нахуй")

Вот так вот, Колян. Создал объект, проинициализировал поля через super().__setattr__, а потом любой вызов обычного __setattr__ посылаешь на три весёлые буквы. Красота, да и только.

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