← Назад к вопросам
Как написать неизменяемый класс?
2.0 Middle🔥 81 комментариев
#Python Core#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как написать неизменяемый класс
Неизменяемость (immutability) — это важный паттерн для создания безопасного, предсказуемого кода. Разберёмся с несколькими способами реализации в Python.
Способ 1: Использование __slots__ и свойств
Базовый подход для контроля над атрибутами:
class Point:
__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"Объект неизменяем, нельзя установить {name}")
def __delattr__(self, name):
raise AttributeError("Объект неизменяем, удаление запрещено")
# Использование:
p = Point(10, 20)
print(p.x, p.y) # 10 20
# p.x = 5 # AttributeError: Объект неизменяем
Этот подход:
- Блокирует любые изменения после инициализации
- Использует
__slots__для оптимизации памяти - Требует явного использования
object.__setattr__в__init__
Способ 2: Декоратор @dataclass с frozen=True
Самый современный и удобный способ:
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
id: int
name: str
email: str
def __post_init__(self):
# Даже здесь нельзя менять атрибуты
pass
# Использование:
user = User(id=1, name='Alice', email='alice@example.com')
print(user.name) # Alice
# user.name = 'Bob' # FrozenInstanceError
Плюсы:
- Очень компактно
- Автоматически создаёт
__init__,__repr__,__eq__ - Type hints встроены
- Хэшируемо по умолчанию
Пример с методами:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_to_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
def translate(self, dx: float, dy: float) -> 'Point':
# Возвращаем новый объект вместо изменения
return Point(self.x + dx, self.y + dy)
# Использование:
p1 = Point(3, 4)
print(p1.distance_to_origin()) # 5.0
p2 = p1.translate(1, 1)
print(p2) # Point(x=4, y=5)
print(p1) # Point(x=3, y=4) — p1 не изменилась
Способ 3: Именованный кортеж (namedtuple)
Высокоуровневый способ — кортежи уже неизменяемы:
from typing import NamedTuple
class Address(NamedTuple):
city: str
country: str
postal_code: str
def full_address(self) -> str:
return f"{self.city}, {self.country} {self.postal_code}"
# Использование:
addr = Address('Moscow', 'Russia', '119019')
print(addr.full_address()) # Moscow, Russia 119019
# addr.city = 'SPB' # AttributeError
Очень компактно:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# p.x = 5 # AttributeError
Способ 4: Использование __setattr__ с проверкой
Для большей контроля и кастомной логики:
class ImmutableConfig:
def __init__(self, **kwargs):
object.__setattr__(self, '_data', kwargs)
def __getattr__(self, name):
data = object.__getattribute__(self, '_data')
return data.get(name)
def __setattr__(self, name, value):
if name == '_data':
object.__setattr__(self, name, value)
else:
raise AttributeError(f"Атрибут {name} неизменяем")
def __delattr__(self, name):
raise AttributeError(f"Удаление атрибута {name} запрещено")
def __repr__(self):
data = object.__getattribute__(self, '_data')
return f"ImmutableConfig({data})"
# Использование:
config = ImmutableConfig(debug=True, timeout=30)
print(config.debug) # True
# config.debug = False # AttributeError
Способ 5: Глубокая неизменяемость
Для защиты mutable атрибутов:
from dataclasses import dataclass
from typing import FrozenSet, Tuple
import copy
@dataclass(frozen=True)
class UserGroup:
name: str
members: FrozenSet[str] # Используем frozenset вместо list
tags: Tuple[str, ...] = () # Используем tuple вместо list
def add_member(self, member: str) -> 'UserGroup':
# Возвращаем новый объект с добавленным членом
new_members = self.members | {member}
return UserGroup(self.name, new_members, self.tags)
# Использование:
group = UserGroup('admins', frozenset(['alice', 'bob']))
print(group.members) # frozenset({'alice', 'bob'})
new_group = group.add_member('charlie')
print(group.members) # frozenset({'alice', 'bob'}) — не изменилась
print(new_group.members) # frozenset({'alice', 'bob', 'charlie'})
Способ 6: Copy-on-write паттерн
Для оптимизации памяти при неизменяемости:
from copy import copy
class CopyOnWrite:
def __init__(self, data):
self._data = data
self._hash = None
def __hash__(self):
if self._hash is None:
self._hash = hash(frozenset(self._data.items()))
return self._hash
def __eq__(self, other):
return self._data == other._data
def with_update(self, **changes):
# Создаём новый объект с изменениями
new_data = self._data.copy()
new_data.update(changes)
return CopyOnWrite(new_data)
def __repr__(self):
return f"CopyOnWrite({self._data})"
# Использование:
obj1 = CopyOnWrite({'x': 1, 'y': 2})
obj2 = obj1.with_update(x=10)
print(obj1) # CopyOnWrite({'x': 1, 'y': 2})
print(obj2) # CopyOnWrite({'x': 10, 'y': 2})
# Можно использовать как ключи в dict (благодаря __hash__)
db = {obj1: 'first', obj2: 'second'}
Способ 7: Использование attrs библиотеки
Потенциально лучше, чем dataclass для сложных случаев:
import attr
@attr.s(slots=True, frozen=True)
class Coordinate:
x: float = attr.ib()
y: float = attr.ib()
def move(self, dx: float, dy: float) -> 'Coordinate':
return Coordinate(self.x + dx, self.y + dy)
# Использование:
coord = Coordinate(10, 20)
new_coord = coord.move(5, 5)
print(coord) # Coordinate(x=10, y=20)
print(new_coord) # Coordinate(x=15, y=25)
Сравнение подходов
┌─────────────────┬────────────┬──────────┬────────────┬────────────┐
│ Способ │ Простота │ Скорость │ Функции │ Type hints │
├─────────────────┼────────────┼──────────┼────────────┼────────────┤
│ __slots__ │ Средняя │ Быстро │ Кастомные │ Нет │
│ @dataclass │ Очень │ Быстро │ Хорошие │ Да │
│ NamedTuple │ Очень │ Очень │ Ограничены │ Да │
│ Кастомный │ Сложная │ Зависит │ Полные │ Нет │
│ attrs │ Средняя │ Быстро │ Очень │ Да │
└─────────────────┴────────────┴──────────┴────────────┴────────────┘
Лучшие практики
1. Предпочитай возврат нового объекта вместо изменения:
# Плохо
user.name = 'New Name'
# Хорошо
new_user = user.with_name('New Name')
2. Используй @dataclass(frozen=True) как default:
@dataclass(frozen=True)
class Event:
timestamp: datetime
event_type: str
data: dict
3. Глубокая неизменяемость для mutable полей:
@dataclass(frozen=True)
class Container:
items: Tuple[int, ...] = () # Tuple вместо list
tags: FrozenSet[str] = field(default_factory=frozenset)
4. Реализуй __hash__ для неизменяемых объектов:
@dataclass(frozen=True)
class Point: # Автоматически хэшируемо
x: float
y: float
points = {Point(1, 2), Point(2, 3)} # Можно использовать в set