Объекты значения (Value Object) в программировании на Python

Раздел: Продвинутые темы -> ООП/Объекты

Объекты значения в Python

Основное решение: dataclass с frozen=True

Самый современный и удобный способ создания value object в Python - использование декоратора @dataclass с параметром frozen=True. Такой класс автоматически получает методы __init__, __repr__, __eq__ и __hash__ (благодаря frozen). Экземпляры становятся неизменяемыми, что соответствует концепции объекта значения.

from dataclasses import dataclass

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

p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1 == p2)  # True
print(hash(p1))  # некоторое число

объект значения в python (объект значения в python)

Основные шаги: аннотации типов, декоратор, frozen. Проблемы возникают, если атрибут содержит изменяемый тип (например, список). В таком случае @dataclass не создаёт __hash__, и объект нельзя использовать в множествах или как ключ словаря. Решение - вручную определить __hash__ или использовать неизменяемые типы (tuple, frozenset).

Типичная ошибка:

Попытка изменить атрибут frozen-объекта вызывает FrozenInstanceError. Это ожидаемо, но иногда нужно изменить значение в __post_init__ - для этого используется object.__setattr__.

Вариант 1: namedtuple - минимальный код без аннотаций

Вопрос: «Как создать неизменяемый объект с минимальным объёмом кода?»

Использование collections.namedtuple позволяет быстро определить класс с кортежем полей.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y)  # 1 2
print(hash(p))   # работает

Цель: простая структура данных, сериализуемая в кортеж. Случаи использования: замена кортежам с именованными полями, когда не нужна валидация.

Проблема:

Нет аннотаций типов (до Python 3.6), нельзя добавлять методы без обёртки, сложно расширять. Для добавления методов приходится создавать подкласс, что лишает некоторых преимуществ.

Вариант 2: обычный класс с __slots__ и полным определением методов

Вопрос: «Как реализовать value object без использования внешних библиотек или декораторов?»

class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)
    def __hash__(self):
        return hash((self.x, self.y))

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)  # True

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

Проблема:

Много шаблонного кода; легко забыть определить __hash__ или допустить ошибку в __eq__. Необходимость явно указывать __slots__ для экономии памяти.

Вариант 3: библиотека attrs - расширенная валидация и конвертация

Вопрос: «Как получить продвинутую валидацию и автогенерацию методов без написания boilerplate?»

from attr import define, frozen, validators

@define(frozen=True, auto_attribs=True)
class Point:
    x: float = field(validator=validators.instance_of(float))
    y: float

p = Point(1.0, 2.0)
print(p)
# Point(x=1.0, y=2.0)

Цель: удобство dataclass + дополнительные возможности (валидаторы, конвертеры, слоты). Случаи использования: сложные доменные объекты с проверкой инвариантов.

Проблема:

Внешняя зависимость (attrs). Необходимость изучения дополнительного API. При некорректном использовании валидаторов возможны неочевидные ошибки.

Вариант 4: pydantic - строгая типизация и сериализация

Вопрос: «Как обеспечить автоматическую валидацию данных из внешних источников (JSON, API)?»

from pydantic import BaseModel

class Point(BaseModel, frozen=True):
    x: float
    y: float

p = Point(x="3.14", y=2.0)  # x будет приведено к float
print(p)
# Point(x=3.14, y=2.0)

Цель: строгая валидация, автоматическая сериализация/десериализация, генерация JSON Schema. Случаи использования: интеграция с веб-фреймворками, конфигурационные объекты.

Проблема:

Значительный размер библиотеки, более медленная инициализация по сравнению с dataclass. Не все функции могут быть нужны для простых value object.

Дополнительные примеры

Создание value object для денег с операциями

Используем dataclass с frozen=True и добавляем арифметические операции.

Пример
from dataclasses import dataclass
from decimal import Decimal
from typing import Union

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __post_init__(self):
        # Преобразование числа в Decimal, если передано не Decimal
        if not isinstance(self.amount, Decimal):
            object.__setattr__(self, 'amount', Decimal(str(self.amount)))
        if self.currency not in ('USD', 'EUR', 'RUB'):
            raise ValueError(f"Неподдерживаемая валюта: {self.currency}")

    def __add__(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Валюты должны совпадать")
        return Money(self.amount + other.amount, self.currency)

    def __repr__(self):
        return f"{self.amount:.2f} {self.currency}"

# Пример использования
m1 = Money(100.50, 'USD')
m2 = Money(50.25, 'USD')
print(m1 + m2)  # 150.75 USD
150.75 USD

Использование value object в качестве ключей словаря

Благодаря реализации __hash__ объекты Point можно использовать в множествах и словарях.

Пример
from dataclasses import dataclass

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

points_set = {Point(0,0), Point(1,1), Point(0,0)}
print(points_set)  # Две разные точки (дубликат удалён)
{Point(x=0.0, y=0.0), Point(x=1.0, y=1.0)}

Сопоставление с образцом (pattern matching) для value object

Определим __match_args__, чтобы использовать match.

Пример
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float
    __match_args__ = ('x', 'y')

def describe(point):
    match point:
        case Point(0, 0):
            return "Начало координат"
        case Point(x, y):
            return f"Точка ({x}, {y})"

print(describe(Point(3, 4)))
Точка (3.0, 4.0)

Композиция value objects: заказ с позициями

Покажем, как value object могут входить в состав других объектов.

Пример
from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __mul__(self, factor: int) -> 'Money':
        return Money(self.amount * factor, self.currency)

@dataclass(frozen=True)
class Product:
    name: str
    price: Money

@dataclass(frozen=True)
class OrderLine:
    product: Product
    quantity: int

    def total(self) -> Money:
        return self.product.price * self.quantity

tea = Product("Чай", Money(Decimal('2.50'), 'RUB'))
line = OrderLine(tea, 3)
print(line.total())  # 7.50 RUB
7.50 RUB

Сериализация и восстановление value object с помощью pydantic

Используем модель pydantic с frozen=True для автоматической валидации из словаря.

Пример
from pydantic import BaseModel

class Point(BaseModel, frozen=True):
    x: float
    y: float

data = {"x": "3.14", "y": "2.71"}
p = Point(**data)
print(p)
print(p.json())  # сериализация в JSON
x=3.14 y=2.71
{"x": 3.14, "y": 2.71}

Объект значения в Python - comments

En
объект значения в python (python)