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

Можно ли сделать объект неизменяемым в Python?

2.3 Middle🔥 151 комментариев
#Python Core#Архитектура и паттерны

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

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

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

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

Python предоставляет несколько способов сделать объект неизменяемым (immutable). Это важно для обеспечения безопасности, кеширования и параллельной обработки.

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

Python уже имеет встроенные неизменяемые типы:

# Неизменяемые типы
immutable_int = 42
immutable_str = "hello"
immutable_tuple = (1, 2, 3)
immutable_frozenset = frozenset([1, 2, 3])
immutable_bytes = b"data"

# Попытка изменить приведёт к ошибке
# immutable_tuple[0] = 5  # TypeError
# immutable_str[0] = 'H'  # TypeError

print(id(immutable_str))
immutable_str = immutable_str + " world"
print(id(immutable_str))  # Другой объект!

2. Использование slots

slots ограничивает добавление атрибутов и улучшает производительность памяти.

class ImmutablePoint:
    """Точка с фиксированными координатами"""
    
    __slots__ = ('_x', '_y')
    
    def __init__(self, x, y):
        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(f"Cannot modify attribute {name}")
    
    def __repr__(self):
        return f"ImmutablePoint({self._x}, {self._y})"

point = ImmutablePoint(3, 4)
print(point.x, point.y)  # 3 4
# point.x = 5  # AttributeError

3. dataclass с frozen=True

Самый удобный способ для современного Python:

from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    """Неизменяемый класс пользователя"""
    id: int
    name: str
    email: str
    age: int = 0
    
    def __hash__(self):
        # Необходимо для использования в set и как ключ dict
        return hash((self.id, self.name, self.email, self.age))

user = User(1, "Alice", "alice@example.com", 25)
print(user)

# Попытка изменить приведёт к ошибке
# user.name = "Bob"  # FrozenInstanceError

# Зато можно использовать в set и dict
user_set = {user}
user_dict = {user: "some_value"}

print(f"Hash: {hash(user)}")

4. NamedTuple для неизменяемости

from typing import NamedTuple

class Person(NamedTuple):
    """Неизменяемая именованная кортеж"""
    name: str
    age: int
    email: str
    
    def full_info(self):
        return f"{self.name} ({self.age}) - {self.email}"

person = Person("Bob", 30, "bob@example.com")
print(person.full_info())

# Неизменяемость
# person.name = "Alice"  # AttributeError

# Но можно создать новый объект
person_updated = person._replace(name="Alice")
print(person_updated)

# Можно использовать в dict и set
people = {person, person_updated}
print(len(people))  # 2

5. Метаклассы для полной неизменяемости

class ImmutableMeta(type):
    """Метакласс для создания неизменяемых классов"""
    
    def __new__(mcs, name, bases, namespace):
        # Запретить изменение методов класса
        namespace['_initialized'] = False
        return super().__new__(mcs, name, bases, namespace)
    
    def __setattr__(cls, name, value):
        raise TypeError(f"Cannot modify class {cls.__name__}")

class ImmutableBase(metaclass=ImmutableMeta):
    """Базовый класс для неизменяемых объектов"""
    
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            object.__setattr__(self, key, value)
        object.__setattr__(self, '_initialized', True)
    
    def __setattr__(self, name, value):
        if object.__getattribute__(self, '_initialized'):
            raise AttributeError(f"Cannot modify attribute {name}")
        object.__setattr__(self, name, value)
    
    def __delattr__(self, name):
        raise AttributeError(f"Cannot delete attribute {name}")

class Rectangle(ImmutableBase):
    def __init__(self, width, height):
        super().__init__(width=width, height=height)
    
    @property
    def area(self):
        return self.width * self.height

rect = Rectangle(10, 5)
print(f"Area: {rect.area}")  # 50
# rect.width = 20  # AttributeError

6. Использование copy.copy с copy.deepcopy

import copy

class ImmutableData:
    """Неизменяемые данные с контролем копирования"""
    
    def __init__(self, data):
        self._data = tuple(data) if isinstance(data, list) else data
    
    def __copy__(self):
        # Неглубокое копирование возвращает тот же объект
        return self
    
    def __deepcopy__(self, memo):
        # Глубокое копирование тоже возвращает тот же объект
        return self
    
    def __repr__(self):
        return f"ImmutableData({self._data})"

original = ImmutableData([1, 2, 3])
copy1 = copy.copy(original)
copy2 = copy.deepcopy(original)

print(original is copy1)  # True
print(original is copy2)  # True
print(id(original) == id(copy1) == id(copy2))  # True

7. Контроль изменений через свойства

class ConfigValue:
    """Конфигурационное значение с контролем изменений"""
    
    def __init__(self, initial_value, mutable_after=None):
        self._value = initial_value
        self._mutable_after = mutable_after
        self._changed = False
        self._locked = False
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        if self._locked:
            raise ValueError("Value is locked and cannot be changed")
        
        if self._changed and not self._mutable_after:
            raise ValueError("Value can only be set once")
        
        self._value = new_value
        self._changed = True
    
    def lock(self):
        """Заблокировать изменение значения"""
        self._locked = True
    
    def unlock(self):
        """Разблокировать значение"""
        self._locked = False

# Использование
config = ConfigValue("initial", mutable_after=None)
config.value = "changed"  # OK, первый раз
# config.value = "changed_again"  # ValueError

config.lock()
# config.value = "locked"  # ValueError

8. Context managers для временной изменяемости

from contextlib import contextmanager

class LockedObject:
    """Объект, который может быть временно разблокирован"""
    
    def __init__(self, **kwargs):
        self._data = kwargs
        self._locked = True
    
    @contextmanager
    def unlock(self):
        """Временно разблокировать объект"""
        self._locked = False
        try:
            yield self
        finally:
            self._locked = True
    
    def __setattr__(self, name, value):
        if name in ('_data', '_locked'):
            object.__setattr__(self, name, value)
            return
        
        if object.__getattribute__(self, '_locked'):
            raise AttributeError(f"Object is locked, cannot set {name}")
        
        self._data[name] = value
    
    def __getattr__(self, name):
        if name.startswith('_'):
            return object.__getattribute__(self, name)
        return self._data.get(name)

obj = LockedObject(name="initial", count=0)

print(obj.name)  # "initial"
# obj.name = "modified"  # AttributeError

with obj.unlock():
    obj.name = "modified"
    obj.count = 10

print(obj.name)  # "modified"
print(obj.count)  # 10
# obj.name = "again"  # AttributeError (объект вновь заблокирован)

9. Сравнение подходов

"""
Подход                  | Простота | Производительность | Гибкость
------------------------+-----------+--------------------+---------
Замороженный dataclass  | Отличная  | Хорошая             | Средняя
NamedTuple              | Хорошая   | Отличная            | Низкая
__slots__ + свойства    | Средняя   | Отличная            | Хорошая
Метакласс               | Сложная   | Хорошая             | Отличная
Contextmanager          | Средняя   | Хорошая             | Отличная

Популярная рекомендация:
- Простые структуры данных: @dataclass(frozen=True)
- Производительность критична: NamedTuple
- Сложная логика: Метаклассы или контекст-менеджеры
"""

10. Распространённые ошибки

# ПЛОХО: Mutable default в неизменяемом класссе
@dataclass(frozen=True)
class BadPerson:
    name: str
    hobbies: list = []  # ОПАСНО! Shared mutable default

# ХОРОШО: Использовать default_factory
from dataclasses import field

@dataclass(frozen=True)
class GoodPerson:
    name: str
    hobbies: list = field(default_factory=list)

# ПЛОХО: Содержать изменяемые атрибуты в frozen dataclass
@dataclass(frozen=True)
class Container:
    data: list  # Атрибут изменяемый, но сам объект нет

container = Container([1, 2, 3])
container.data.append(4)  # Это сработает! (изменяем содержимое)

# ХОРОШО: Использовать только неизменяемые атрибуты
@dataclass(frozen=True)
class SafeContainer:
    data: tuple  # Неизменяемый тип

Заключение

Для создания неизменяемых объектов в Python:

  1. Простой случай: @dataclass(frozen=True)
  2. Производительность: NamedTuple
  3. Сложная логика: Метаклассы или контекст-менеджеры
  4. Лучшая практика: Ограничивать изменяемость только необходимыми местами
  5. Помнить: Неизменяемость объекта не гарантирует неизменяемость его атрибутов

Неизменяемость важна для:

  • Потокобезопасности: никакой синхронизации не требуется
  • Кеширования: можно использовать как ключи
  • Тестирования: предсказуемое поведение
  • Функционального программирования: чистые функции
Можно ли сделать объект неизменяемым в Python? | PrepBro