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

Будет ли класс иммутабельным (неизменяемым), если в нем находится атрибут?

1.8 Middle🔥 61 комментариев
#Python Core

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

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

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

# Иммутабельность в Python: классы и атрибуты

Краткий ответ

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

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

p = Point(1, 2)
p.x = 10  # можем изменить атрибут
print(p.x)  # 10 — класс НЕ иммутабельный

Что такое иммутабельность?

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

  • Thread-safety — можно безопасно использовать в многопоточных приложениях
  • Cacheable — можно кешировать (напр., использовать в качестве ключей в dict)
  • Predictability — код более предсказуем, нет скрытых побочных эффектов

Иммутабельные встроенные типы в Python

# ✅ Иммутабельные типы
int_val = 42
int_val = 43  # новый объект, не изменение

str_val = "hello"
str_val = str_val + " world"  # новая строка, не изменение

tuple_val = (1, 2, 3)
# tuple_val[0] = 10  # TypeError!

# ❌ Мутабельные типы
list_val = [1, 2, 3]
list_val[0] = 10  # изменение на месте ✓

dict_val = {'a': 1}
dict_val['a'] = 2  # изменение на месте ✓

set_val = {1, 2, 3}
set_val.add(4)  # изменение на месте ✓

Способ 1: Использовать __slots__ (базовый)

__slots__ запрещает добавление новых атрибутов, но не делает класс иммутабельным:

class Point:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p.x)  # 1

# Можем изменить существующий атрибут
p.x = 10
print(p.x)  # 10 — всё ещё мутабельно!

# Но не можем добавить новый
# p.z = 5  # AttributeError

Способ 2: Использовать frozen dataclass

from dataclasses import dataclass

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

p = Point(1, 2)
print(p.x)  # 1

# Попытка изменить атрибут
# p.x = 10  # FrozenInstanceError: cannot assign to field 'x'

# Можем использовать как ключ в dict
points = {p: "point_1"}
print(points[Point(1, 2)])  # "point_1"

# Можем использовать в set
point_set = {Point(1, 2), Point(3, 4)}
print(Point(1, 2) in point_set)  # True

Способ 3: Использовать NamedTuple

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(1, 2)
print(p.x)  # 1

# Попытка изменить
# p.x = 10  # AttributeError: can't set attribute

# Это кортеж, поэтому полностью иммутабельный
print(hash(p))  # можем хешировать
print(isinstance(p, tuple))  # True

Способ 4: __setattr__ и __delattr__

class ImmutablePoint:
    def __init__(self, x, y):
        object.__setattr__(self, 'x', x)
        object.__setattr__(self, 'y', y)
    
    def __setattr__(self, name, value):
        raise AttributeError(f"Cannot modify {name}")
    
    def __delattr__(self, name):
        raise AttributeError(f"Cannot delete {name}")
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = ImmutablePoint(1, 2)
print(p)  # Point(1, 2)

# Попытка изменить
# p.x = 10  # AttributeError: Cannot modify x

# Попытка удалить
# del p.x  # AttributeError: Cannot delete x

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

ПодходПлюсыМинусыHashEquals
frozen=True dataclassПростой, модерныйPython 3.7+
NamedTupleЛёгкий, tuple-likeМенее гибкий
__setattr__ перегрузкаПолный контрольМногословный✗ нужно явно✗ нужно явно
__slots__Экономия памятиНе иммутабельный✗ нужно явно✗ нужно явно

Проблема: иммутабельность поверхностная

Если атрибут содержит мутабельный объект, внешний состояние всё равно может быть изменено:

from dataclasses import dataclass

@dataclass(frozen=True)
class Box:
    items: list  # мутабельный список

box = Box([1, 2, 3])
print(box.items)  # [1, 2, 3]

# Хотя box иммутабельная, мы можем изменить список внутри
box.items.append(4)
print(box.items)  # [1, 2, 3, 4] — изменилось!

# Это называется "shallow immutability" (поверхностная иммутабельность)

Решение: глубокая иммутабельность

from dataclasses import dataclass, field
from copy import deepcopy

@dataclass(frozen=True)
class Box:
    items: tuple  # используем tuple вместо list

box = Box((1, 2, 3))
print(box.items)  # (1, 2, 3)

# Теперь действительно иммутабельно
# box.items.append(4)  # AttributeError: 'tuple' object has no attribute 'append'

# Или используем post_init для копирования
@dataclass(frozen=True)
class SafeBox:
    _items: list = field(default_factory=list, init=False, repr=False)
    
    def __post_init__(self):
        # Сохраняем копию
        items = getattr(self, '__dict__', {}).get('items', [])
        object.__setattr__(self, '_items', tuple(items))
    
    @property
    def items(self) -> tuple:
        return self._items

Практический пример: ValueObject в DDD

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class Email:
    """Иммутабельный value object для email"""
    value: str
    
    def __post_init__(self):
        if '@' not in self.value:
            raise ValueError("Invalid email")
    
    def __str__(self):
        return self.value

@dataclass(frozen=True)
class User:
    name: str
    email: Email

# Использование
try:
    invalid_email = Email("invalid")
except ValueError:
    print("Invalid email")

email = Email("user@example.com")
user = User("John", email)
print(user)  # User(name='John', email=user@example.com)

# Можем использовать как ключ
users_map = {user: "admin"}
print(users_map[User("John", Email("user@example.com"))])  # "admin"

# Изменение защищено
# user.email = Email("other@example.com")  # FrozenInstanceError

Иммутабельность vs наследование

from dataclasses import dataclass

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

# Попытка создать подкласс
@dataclass(frozen=True)
class ColoredPoint(Point):
    color: str

p = ColoredPoint(1, 2, "red")
print(p)  # ColoredPoint(x=1, y=2, color='red')

# Всё ещё иммутабельно
# p.color = "blue"  # FrozenInstanceError

Итоги: как сделать класс иммутабельным

Используй одно из:

  1. @dataclass(frozen=True) — самый простой и рекомендуемый способ
  2. NamedTuple — если нужно наследоваться от tuple
  3. __setattr__/__delattr__ — если нужен полный контроль

⚠️ Помни:

  • Просто наличие атрибутов не делает класс иммутабельным
  • Иммутабельность по умолчанию поверхностная (shallow)
  • Используй мутабельные типы (list, dict) с осторожностью в frozen объектах
  • Иммутабельные объекты можно хешировать и использовать в dict/set
# ✅ Рекомендуемый способ
from dataclasses import dataclass

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

p = Point(1, 2)
# p.x = 10  # FrozenInstanceError
print(hash(p))  # можем использовать как ключ
Будет ли класс иммутабельным (неизменяемым), если в нем находится атрибут? | PrepBro