Работа с вложенными объектами в Python: композиция и агрегация

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

Основные принципы работы с вложенными объектами

В объектно-ориентированном программировании часто требуется, чтобы один объект содержал другой. Это называется композицией (сильная связь) или агрегацией (слабая связь). В Python это реализуется через присваивание атрибуту объекта другого объекта. Рассмотрим основной подход и альтернативные варианты.

Основное решение: передача вложенного объекта через конструктор

Наиболее гибкий и рекомендуемый способ - получать готовый экземпляр внутреннего объекта как аргумент __init__. Это позволяет контролировать создание зависимости извне, упрощает тестирование и замену компонентов.


class Engine:
    def __init__(self, power):
        self.power = power

    def start(self):
        print(f'Двигатель {self.power} л.с. запущен')

class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine

    def drive(self):
        self.engine.start()
        print(f'{self.brand} поехала')

engine_v8 = Engine(450)
my_car = Car('Ford', engine_v8)
my_car.drive()

атрибуты класса python (атрибуты классов и объектов в python)

Двигатель 450 л.с. запущен
Ford поехала

библиотека классов python (библиотека классов в python)

Типичная ошибка: изменение переданного объекта после создания Car может привести к неожиданным эффектам, если объект используется в другом месте. Решение - копирование вложенного объекта, если требуется изоляция.

Вариант 1. Как создать вложенный объект прямо внутри конструктора?

Если внутренний объект полностью принадлежит внешнему и не должен существовать отдельно, можно создать его прямо в __init__.


class Engine:
    def __init__(self, power=100):
        self.power = power

class Car:
    def __init__(self, brand):
        self.brand = brand
        self.engine = Engine()

car = Car('Toyota')
print(car.engine.power)  # 100

метод объекта python (методы объектов в python)

Проблема: теряется гибкость - нельзя подставить другую реализацию двигателя. Кроме того, если Engine требует сложной конфигурации, код становится жёстко связанным.

Вариант 2. Как передать вложенный объект извне для переиспользования?

Этот вариант уже описан в основном решении - внедрение зависимости через параметр конструктора. Позволяет использовать один и тот же объект в нескольких внешних объектах.


engine = Engine(200)
car1 = Car('BMW', engine)
car2 = Car('Audi', engine)

Python структура объекта (структура объекта в python)

Ошибка: если один из объектов изменит состояние вложенного объекта, это повлияет на другой. Решение - клонирование или использование неизменяемых объектов.

Вариант 3. Как создать объект с предварительно настроенными вложенными объектами через фабрику?

Фабричный метод (@classmethod) позволяет создавать объекты с определённой конфигурацией.


class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine

    @classmethod
    def sport_car(cls, brand):
        engine = Engine(500)
        return cls(brand, engine)

    @classmethod
    def economy_car(cls, brand):
        engine = Engine(80)
        return cls(brand, engine)

sport = Car.sport_car('Ferrari')
eco = Car.economy_car('Fiat')

Python создание объектов (создание объектов в python)

Сложность: при увеличении числа параметров фабричные методы могут разрастаться. Альтернатива - использование паттерна Builder.

Вариант 4. Как отложить создание вложенного объекта до первого обращения?

Использование свойства (@property) позволяет инициализировать вложенный объект только при необходимости.


class Car:
    def __init__(self, brand):
        self.brand = brand
        self._engine = None

    @property
    def engine(self):
        if self._engine is None:
            self._engine = Engine()
        return self._engine

car = Car('Honda')
print(car.engine.power)  # создаётся при обращении

Self object python (объект self в python)

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

Вариант 5. Как упростить объявление вложенных объектов с помощью dataclasses?

Модуль dataclasses автоматически генерирует __init__ и позволяет компактно описывать композицию.


from dataclasses import dataclass

@dataclass
class Engine:
    power: int

@dataclass
class Car:
    brand: str
    engine: Engine

car = Car('Mazda', Engine(150))
print(car)

Object attribute python (атрибуты объекта в python)

Car(brand='Mazda', engine=Engine(power=150))

Особенность: dataclasses создают изменяемые объекты по умолчанию. Для неизменяемости нужно указать frozen=True. Также поля с изменяемыми типами (list, dict) могут приводить к проблемам с общими ссылками.

- Class method python (методы классов в python)
- Python object methods (методы объектов в python)
- класс python определение (определение классов в python)

Расширенные примеры вложенных объектов

Многоуровневая композиция

Объект может содержать несколько уровней вложенности, например, автомобиль содержит двигатель, а двигатель - поршни.

Пример

class Piston:
    def __init__(self, diameter):
        self.diameter = diameter

class Engine:
    def __init__(self, power):
        self.power = power
        self.pistons = [Piston(80), Piston(80), Piston(80), Piston(80)]

class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine

car = Car('Mercedes', Engine(300))
print(f'Количество поршней: {len(car.engine.pistons)}')
Количество поршней: 4

Композиция с коллекциями (список колес)

Внешний объект может хранить коллекцию вложенных объектов.

Пример

class Wheel:
    def __init__(self, size):
        self.size = size

class Car:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels  # список из Wheel

    def total_diameter(self):
        return sum(w.size for w in self.wheels)

wheels = [Wheel(17), Wheel(17), Wheel(17), Wheel(17)]
car = Car('BMW', wheels)
print(car.total_diameter())
68

Передача вложенного объекта в методы

Метод внешнего объекта может принимать вложенный объект как параметр.

Пример

class Mechanic:
    def repair(self, engine):
        print('Механик чинит двигатель мощностью', engine.power)

engine = Engine(200)
mechanic = Mechanic()
mechanic.repair(engine)
Механик чинит двигатель мощностью 200

Сравнение вложенных объектов

По умолчанию сравнение объектов выполняется по id. Чтобы сравнивать по содержимому, нужно переопределить __eq__ или использовать dataclasses с order=True.

Пример

@dataclass
class Engine:
    power: int

@dataclass
class Car:
    brand: str
    engine: Engine

car1 = Car('A', Engine(100))
car2 = Car('A', Engine(100))
print(car1 == car2)  # True, так как dataclass генерирует __eq__
True

Использование __slots__ для экономии памяти

Если много вложенных объектов, можно применить __slots__ для уменьшения потребления памяти.

Пример

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

class Line:
    __slots__ = ('start', 'end')
    def __init__(self, start, end):
        self.start = start
        self.end = end

line = Line(Point(0,0), Point(1,1))
print(line.start.x)  # 0

Важно: __slots__ запрещает добавление новых атрибутов, что может быть неудобно при композиции.

Сериализация вложенных объектов

Для сохранения и загрузки вложенных структур удобно использовать json модуль с пользовательским кодировщиком или pickle.

Пример

import json

class Engine:
    def __init__(self, power):
        self.power = power

class Car:
    def __init__(self, brand, engine):
        self.brand = brand
        self.engine = engine

car = Car('Tesla', Engine(250))
# Сериализация через __dict__ не работает для вложенных объектов
# Решение: метод to_dict

def to_dict(obj):
    if hasattr(obj, '__dict__'):
        return {k: to_dict(v) for k, v in obj.__dict__.items()}
    else:
        return obj

print(json.dumps(to_dict(car), indent=2))
{
  "brand": "Tesla",
  "engine": {
    "power": 250
  }
}

Неизменяемые вложенные объекты (frozen dataclass)

Если требуется, чтобы вложенные объекты не изменялись после создания, используется frozen=True.

Пример

@dataclass(frozen=True)
class Engine:
    power: int

@dataclass(frozen=True)
class Car:
    brand: str
    engine: Engine

car = Car('Nissan', Engine(120))
# car.engine.power = 200  # вызовет ошибку

Проблемы с изменяемыми объектами по умолчанию

Одна из частых ошибок - использование изменяемого объекта как значения по умолчанию в аргументах конструктора.

Пример

class Car:
    def __init__(self, brand, engine=Engine(100)):  # неправильно: один объект для всех вызовов
        self.brand = brand
        self.engine = engine

car1 = Car('A')
car2 = Car('B')
car1.engine.power = 200
print(car2.engine.power)  # 200, так как все используют один и тот же engine

Решение: использовать None по умолчанию и создавать новый объект внутри __init__.

Работа с вложенными объектами в Python - comments

En
Python объект внутри объекта (python)