$ sudo teach IT
МОДУЛЬ 6 · УРОК 4

Практика: Компьютер из компонентов

В прошлых уроках мы разобрали теорию агрегации. Сегодня строим реальную систему: класс Computer, который состоит из независимых компонентов — CPU, RAM, Disk и опционального GPU. Это учебный полигон, где агрегация работает во всей красе.

⏱ ~45 минут 🔨 Урок-практика 🐍 Python 3

🎯 Что мы строим сегодня

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

🔍 Это классическая агрегация: компоненты создаются отдельно, а затем передаются в компьютер. Компьютер может быть «разобран» — а компоненты останутся и могут войти в новый ПК.

По ходу урока мы пройдём пять шагов: создадим компоненты, соберём компьютер, напишем методы boot(), describe() и upgrade_ram(), а в конце добавим опциональный GPU.

🗂 Проектируем иерархию классов

Прежде чем писать код — договоримся о структуре. Хорошие программисты сначала думают, потом кодят.

🖥

CPU

Модель, количество ядер, тактовая частота (ГГц)

🧠

RAM

Объём (ГБ), тип памяти (DDR4, DDR5 и т.д.)

💾

Disk

Объём (ГБ), тип — SSD или HDD

🎮

GPU (опционально)

Модель, объём видеопамяти (ГБ). Может отсутствовать.

💻

Computer

Агрегирует все компоненты. Методы: boot(), describe(), upgrade_ram()

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

🔧 Шаг 1: Классы компонентов — CPU, RAM, Disk

Начнём с «кирпичей». Каждый компонент — самостоятельный класс. Обратите внимание: у каждого есть метод __repr__, чтобы объект красиво отображался при печати.

class CPU:
    """Процессор — мозг компьютера."""

    def __init__(self, model, cores, frequency):
        self.model = model          # например "Intel Core i7-13700K"
        self.cores = cores          # количество ядер, например 16
        self.frequency = frequency  # тактовая частота в ГГц, например 3.4

    def __repr__(self):
        return f"CPU({self.model}, {self.cores} ядер, {self.frequency} ГГц)"


class RAM:
    """Оперативная память."""

    def __init__(self, size, memory_type):
        self.size = size              # объём в ГБ, например 32
        self.memory_type = memory_type  # тип: "DDR4", "DDR5" и т.д.

    def __repr__(self):
        return f"RAM({self.size} ГБ, {self.memory_type})"


class Disk:
    """Накопитель данных."""

    def __init__(self, size, ssd_or_hdd):
        self.size = size              # объём в ГБ, например 1000
        self.ssd_or_hdd = ssd_or_hdd  # строка: "SSD" или "HDD"

    def __repr__(self):
        return f"Disk({self.size} ГБ, {self.ssd_or_hdd})"

✅ Зачем __repr__? Это «отладочное представление» объекта. Когда мы напишем print(cpu) или увидим объект в ошибке, Python вызовет именно __repr__. Без него вы получите бесполезное <__main__.CPU object at 0x10f5...>. Всегда добавляйте __repr__ к своим классам — это хорошая привычка.

Проверим, что классы работают:

# Создаём компоненты НЕЗАВИСИМО — они живут сами по себе
cpu  = CPU("Intel Core i7-13700K", 16, 3.4)
ram  = RAM(32, "DDR5")
disk = Disk(1000, "SSD")

print(cpu)   # CPU(Intel Core i7-13700K, 16 ядер, 3.4 ГГц)
print(ram)   # RAM(32 ГБ, DDR5)
print(disk)  # Disk(1000 ГБ, SSD)

💻 Шаг 2: Класс Computer — агрегируем компоненты

Теперь создаём класс Computer. Он принимает готовые объекты компонентов в параметрах __init__. Это и есть агрегация в чистом виде: компьютер не создаёт компоненты — он их принимает.

class Computer:
    """Компьютер, собранный из независимых компонентов.

    Использует АГРЕГАЦИЮ: компоненты передаются снаружи
    и могут жить после «разборки» этого компьютера.
    """

    def __init__(self, name, cpu, ram, disk):
        self.name = name  # название конфигурации, например "Игровой ПК"
        # ✅ АГРЕГАЦИЯ: принимаем уже созданные объекты
        self.cpu  = cpu
        self.ram  = ram
        self.disk = disk
        # GPU пока не добавляем — это будет шаг 5

🔍 Обратите внимание на сигнатуру: def __init__(self, name, cpu, ram, disk). Мы принимаем объекты, а не числа или строки. Это значит, что снаружи уже должны быть созданы экземпляры CPU, RAM, Disk — и затем переданы в компьютер. Жизненный цикл компонентов не зависит от жизненного цикла компьютера.

Соберём компьютер из ранее созданных компонентов:

# Компоненты уже созданы (шаг 1)
cpu  = CPU("Intel Core i7-13700K", 16, 3.4)
ram  = RAM(32, "DDR5")
disk = Disk(1000, "SSD")

# Передаём их в Computer — агрегация
my_pc = Computer("Рабочая станция", cpu, ram, disk)

# Компоненты доступны через атрибуты компьютера
print(my_pc.cpu)   # CPU(Intel Core i7-13700K, 16 ядер, 3.4 ГГц)
print(my_pc.ram)   # RAM(32 ГБ, DDR5)
print(my_pc.disk)  # Disk(1000 ГБ, SSD)

# Но они всё ещё доступны и напрямую!
print(cpu)  # CPU(Intel Core i7-13700K, 16 ядер, 3.4 ГГц)
# Это невозможно при композиции — там объект живёт ТОЛЬКО внутри родителя.

▶️ Шаг 3: Методы boot() и describe()

Добавим два метода в класс Computer. Оба обращаются к атрибутам компонентов — то есть компьютер использует свои компоненты, делегируя им часть работы.

class Computer:
    def __init__(self, name, cpu, ram, disk):
        self.name = name
        self.cpu  = cpu
        self.ram  = ram
        self.disk = disk

    def describe(self):
        """Выводит полную конфигурацию компьютера."""
        print(f"\n💻 Конфигурация: {self.name}")
        print(f"  Процессор : {self.cpu.model}, {self.cpu.cores} ядер @ {self.cpu.frequency} ГГц")
        print(f"  Память    : {self.ram.size} ГБ {self.ram.memory_type}")
        drive_type = "⚡ SSD" if self.disk.ssd_or_hdd == "SSD" else "🐌 HDD"
        print(f"  Накопитель: {self.disk.size} ГБ ({drive_type})")

    def boot(self):
        """Симулирует запуск компьютера, используя данные компонентов."""
        print(f"\n🚀 Запуск компьютера «{self.name}»...")
        print(f"  Инициализация CPU: {self.cpu.model}")
        print(f"  Загрузка ОС в RAM: {self.ram.size} ГБ доступно")
        if self.disk.ssd_or_hdd == "SSD":
            print(f"  Чтение с диска: {self.disk.size} ГБ SSD — быстро ✅")
        else:
            print(f"  Чтение с диска: {self.disk.size} ГБ HDD — медленно ⚠️")
        print("  Система готова к работе!")

💡 Почему это правильный дизайн? Метод boot() не дублирует данные о компонентах — он обращается к self.cpu.model, self.ram.size и т.д. Если завтра мы заменим CPU на новый объект, boot() автоматически начнёт использовать новые данные. Агрегация + делегирование = гибкость.

Проверим:

cpu  = CPU("Intel Core i7-13700K", 16, 3.4)
ram  = RAM(32, "DDR5")
disk = Disk(1000, "SSD")

my_pc = Computer("Рабочая станция", cpu, ram, disk)

my_pc.describe()
# 💻 Конфигурация: Рабочая станция
#   Процессор : Intel Core i7-13700K, 16 ядер @ 3.4 ГГц
#   Память    : 32 ГБ DDR5
#   Накопитель: 1000 ГБ (⚡ SSD)

my_pc.boot()
# 🚀 Запуск компьютера «Рабочая станция»...
#   Инициализация CPU: Intel Core i7-13700K
#   Загрузка ОС в RAM: 32 ГБ доступно
#   Чтение с диска: 1000 ГБ SSD — быстро ✅
#   Система готова к работе!

⬆️ Шаг 4: upgrade_ram() — гибкость замены компонентов

Одно из главных преимуществ агрегации — возможность заменить компонент в любой момент. Достаточно создать новый объект и присвоить его атрибуту. Добавим метод upgrade_ram():

    def upgrade_ram(self, new_ram):
        """Заменяет оперативную память на новую.

        Старый объект RAM не уничтожается — он просто
        перестаёт быть частью этого компьютера.
        """
        old_ram = self.ram  # сохраним ссылку на старую RAM
        self.ram = new_ram  # ← простая переприсваивание атрибута!
        print(f"  RAM обновлена: {old_ram} → {new_ram}")
        return old_ram      # возвращаем старую RAM — её можно использовать дальше

✅ Вся замена — одна строка: self.ram = new_ram. Вот почему агрегация так ценится: компонент заменяется атомарно, без переписывания всего класса. При композиции такой трюк значительно сложнее — объект принадлежит только своему «хозяину».

# Демонстрация upgrade
cpu  = CPU("Intel Core i7-13700K", 16, 3.4)
ram  = RAM(16, "DDR4")   # изначально скромная конфигурация
disk = Disk(512, "SSD")

my_pc = Computer("Офисный ПК", cpu, ram, disk)
my_pc.describe()
# 💻 Конфигурация: Офисный ПК
#   Процессор : Intel Core i7-13700K, 16 ядер @ 3.4 ГГц
#   Память    : 16 ГБ DDR4
#   Накопитель: 512 ГБ (⚡ SSD)

# Решили расширить память
new_ram = RAM(64, "DDR5")
old_ram = my_pc.upgrade_ram(new_ram)
#   RAM обновлена: RAM(16 ГБ, DDR4) → RAM(64 ГБ, DDR5)

my_pc.describe()
# 💻 Конфигурация: Офисный ПК
#   Процессор : Intel Core i7-13700K, 16 ядер @ 3.4 ГГц
#   Память    : 64 ГБ DDR5
#   Накопитель: 512 ГБ (⚡ SSD)

# Старая RAM по-прежнему живёт в переменной old_ram
print(old_ram)   # RAM(16 ГБ, DDR4) — никуда не делась!
# Её можно поставить в другой компьютер

🎮 Шаг 5: GPU как опциональный компонент

Не каждый ПК имеет дискретную видеокарту. Сделаем GPU опциональным параметром через значение по умолчанию gpu=None. Это классический Python-паттерн для необязательных зависимостей.

class GPU:
    """Дискретная видеокарта."""

    def __init__(self, model, vram):
        self.model = model  # например "NVIDIA RTX 4080"
        self.vram  = vram   # объём видеопамяти в ГБ, например 16

    def __repr__(self):
        return f"GPU({self.model}, {self.vram} ГБ VRAM)"

Теперь обновим класс Computer, добавив поддержку GPU:

class Computer:
    def __init__(self, name, cpu, ram, disk, gpu=None):
        #                                    ^^^^^^^^^
        #                   gpu=None означает: видеокарта необязательна.
        #                   Если не передать — компьютер без дискретного GPU.
        self.name = name
        self.cpu  = cpu
        self.ram  = ram
        self.disk = disk
        self.gpu  = gpu   # None — интегрированная графика / нет GPU

    def describe(self):
        print(f"\n💻 Конфигурация: {self.name}")
        print(f"  Процессор : {self.cpu.model}, {self.cpu.cores} ядер @ {self.cpu.frequency} ГГц")
        print(f"  Память    : {self.ram.size} ГБ {self.ram.memory_type}")
        drive_type = "⚡ SSD" if self.disk.ssd_or_hdd == "SSD" else "🐌 HDD"
        print(f"  Накопитель: {self.disk.size} ГБ ({drive_type})")
        # Показываем GPU только если он есть
        if self.gpu:
            print(f"  Видеокарта: {self.gpu.model}, {self.gpu.vram} ГБ VRAM")
        else:
            print(f"  Видеокарта: встроенная (дискретной нет)")

    def boot(self):
        print(f"\n🚀 Запуск компьютера «{self.name}»...")
        print(f"  Инициализация CPU: {self.cpu.model}")
        print(f"  Загрузка ОС в RAM: {self.ram.size} ГБ доступно")
        if self.disk.ssd_or_hdd == "SSD":
            print(f"  Чтение с диска: {self.disk.size} ГБ SSD — быстро ✅")
        else:
            print(f"  Чтение с диска: {self.disk.size} ГБ HDD — медленно ⚠️")
        if self.gpu:
            print(f"  Инициализация GPU: {self.gpu.model} ({self.gpu.vram} ГБ VRAM)")
        print("  Система готова к работе!")

    def upgrade_ram(self, new_ram):
        old_ram = self.ram
        self.ram = new_ram
        print(f"  RAM обновлена: {old_ram} → {new_ram}")
        return old_ram

    def add_gpu(self, gpu):
        """Устанавливает дискретную видеокарту."""
        self.gpu = gpu
        print(f"  Установлен GPU: {gpu}")

    def remove_gpu(self):
        """Снимает видеокарту (например, для продажи или перестановки)."""
        if self.gpu:
            removed = self.gpu
            self.gpu = None
            print(f"  GPU {removed} снят с компьютера")
            return removed
        else:
            print("  GPU не установлен — нечего снимать")
            return None

⚠️ Почему именно gpu=None, а не создавать «пустой» GPU? Потому что None — это языковой способ сказать «объект отсутствует». Проверка if self.gpu: читается как «если GPU установлен» — чисто и понятно. Создавать заглушечный объект было бы сложнее и запутаннее.

🏁 Финальный код и демонстрация

Соберём всё вместе. Полная программа — от определения классов до демонстрации всех возможностей:

# ============================================================
# Классы компонентов
# ============================================================

class CPU:
    def __init__(self, model, cores, frequency):
        self.model = model
        self.cores = cores
        self.frequency = frequency

    def __repr__(self):
        return f"CPU({self.model}, {self.cores} ядер, {self.frequency} ГГц)"


class RAM:
    def __init__(self, size, memory_type):
        self.size = size
        self.memory_type = memory_type

    def __repr__(self):
        return f"RAM({self.size} ГБ, {self.memory_type})"


class Disk:
    def __init__(self, size, ssd_or_hdd):
        self.size = size
        self.ssd_or_hdd = ssd_or_hdd

    def __repr__(self):
        return f"Disk({self.size} ГБ, {self.ssd_or_hdd})"


class GPU:
    def __init__(self, model, vram):
        self.model = model
        self.vram = vram

    def __repr__(self):
        return f"GPU({self.model}, {self.vram} ГБ VRAM)"


# ============================================================
# Класс Computer — агрегирует компоненты
# ============================================================

class Computer:
    def __init__(self, name, cpu, ram, disk, gpu=None):
        self.name = name
        self.cpu  = cpu
        self.ram  = ram
        self.disk = disk
        self.gpu  = gpu

    def describe(self):
        print(f"\n💻 Конфигурация: {self.name}")
        print(f"  Процессор : {self.cpu.model}, {self.cpu.cores} ядер @ {self.cpu.frequency} ГГц")
        print(f"  Память    : {self.ram.size} ГБ {self.ram.memory_type}")
        drive_type = "⚡ SSD" if self.disk.ssd_or_hdd == "SSD" else "🐌 HDD"
        print(f"  Накопитель: {self.disk.size} ГБ ({drive_type})")
        if self.gpu:
            print(f"  Видеокарта: {self.gpu.model}, {self.gpu.vram} ГБ VRAM")
        else:
            print(f"  Видеокарта: встроенная (дискретной нет)")

    def boot(self):
        print(f"\n🚀 Запуск компьютера «{self.name}»...")
        print(f"  Инициализация CPU: {self.cpu.model}")
        print(f"  Загрузка ОС в RAM: {self.ram.size} ГБ доступно")
        if self.disk.ssd_or_hdd == "SSD":
            print(f"  Чтение с диска: {self.disk.size} ГБ SSD — быстро ✅")
        else:
            print(f"  Чтение с диска: {self.disk.size} ГБ HDD — медленно ⚠️")
        if self.gpu:
            print(f"  Инициализация GPU: {self.gpu.model} ({self.gpu.vram} ГБ VRAM)")
        print("  Система готова к работе!")

    def upgrade_ram(self, new_ram):
        old_ram = self.ram
        self.ram = new_ram
        print(f"  RAM обновлена: {old_ram} → {new_ram}")
        return old_ram

    def add_gpu(self, gpu):
        self.gpu = gpu
        print(f"  Установлен GPU: {gpu}")

    def remove_gpu(self):
        if self.gpu:
            removed = self.gpu
            self.gpu = None
            print(f"  GPU {removed} снят с компьютера")
            return removed
        print("  GPU не установлен — нечего снимать")
        return None


# ============================================================
# Демонстрация
# ============================================================

print("=" * 50)
print("  СЦЕНАРИЙ 1: Офисный ПК без дискретного GPU")
print("=" * 50)

office_cpu  = CPU("Intel Core i5-13400", 10, 2.5)
office_ram  = RAM(16, "DDR4")
office_disk = Disk(512, "SSD")

office_pc = Computer("Офисный ПК", office_cpu, office_ram, office_disk)
office_pc.describe()
office_pc.boot()

print("\n" + "=" * 50)
print("  СЦЕНАРИЙ 2: Игровой ПК с видеокартой")
print("=" * 50)

gaming_cpu  = CPU("AMD Ryzen 9 7900X", 12, 4.7)
gaming_ram  = RAM(32, "DDR5")
gaming_disk = Disk(2000, "SSD")
gaming_gpu  = GPU("NVIDIA RTX 4080", 16)

gaming_pc = Computer("Игровой ПК", gaming_cpu, gaming_ram, gaming_disk, gpu=gaming_gpu)
gaming_pc.describe()
gaming_pc.boot()

print("\n" + "=" * 50)
print("  СЦЕНАРИЙ 3: Апгрейд офисного ПК")
print("=" * 50)

# Расширяем RAM
new_ram = RAM(32, "DDR5")
old_ram = office_pc.upgrade_ram(new_ram)
print(f"  Старая RAM сохранена: {old_ram}")

# Добавляем GPU
budget_gpu = GPU("NVIDIA GTX 1650", 4)
office_pc.add_gpu(budget_gpu)
office_pc.describe()

print("\n" + "=" * 50)
print("  СЦЕНАРИЙ 4: Перестановка GPU между ПК")
print("=" * 50)

# Снимаем GPU с офисного ПК и ставим в другой
moved_gpu = office_pc.remove_gpu()
# moved_gpu — это тот же объект, он живёт, просто поменял «хозяина»

budget_pc = Computer("Бюджетный ПК", CPU("Intel Core i3-13100", 4, 3.4), RAM(8, "DDR4"), Disk(256, "HDD"))
budget_pc.add_gpu(moved_gpu)
budget_pc.describe()

# Убедимся: gaming_gpu всё ещё в gaming_pc
print(f"\n  gaming_pc GPU: {gaming_pc.gpu}")  # GPU жив и на месте

Запустите этот код у себя и убедитесь, что все сценарии работают корректно.

✏️ Самостоятельная работа

Попробуйте выполнить одно или несколько из этих заданий:

🟢 Лёгкое

Добавьте метод upgrade_disk(new_disk) по аналогии с upgrade_ram(). Метод должен вернуть старый диск.

🟡 Среднее

Добавьте класс PowerSupply (блок питания) с атрибутами brand и wattage. Сделайте его опциональным компонентом компьютера. В методе boot() выведите предупреждение, если мощности БП недостаточно для GPU (например, GPU требует > 200 Вт, а БП < 500 Вт).

🔴 Сложное

Создайте класс ComputerStore (магазин), который агрегирует список объектов Computer. Добавьте методы: add_computer(pc), sell_computer(name) (удаляет из списка и возвращает объект), list_inventory(). Это агрегация второго уровня!

💬 Подсказка для сложного задания: когда вы пишете sell_computer(name), помните — компьютер уходит из магазина, но его компоненты продолжают существовать в объекте Computer. Это и есть суть агрегации: ComputerStore не «владеет» компьютерами в смысле их уничтожения.

✅ Итоги урока

  • CPU, RAM, Disk, GPU — самостоятельные классы с собственными атрибутами и __repr__.
  • Computer принимает компоненты в __init__ как параметры — это агрегация в чистом виде.
  • boot() и describe() обращаются к атрибутам компонентов через self.cpu.model и т.д. — делегирование.
  • upgrade_ram(new_ram) заменяет компонент одной строкой (self.ram = new_ram), старый объект не уничтожается.
  • gpu=None — стандартный Python-паттерн для опционального компонента. Проверка if self.gpu: — идиоматична.
  • GPU можно переставить из одного компьютера в другой — агрегация это позволяет, в отличие от композиции.

👉 В следующем уроке мы подведём итоги всего модуля о связях между классами. Вы научитесь смотреть на любую задачу и сразу понимать: это наследование, ассоциация, композиция или агрегация? Выбор правильного паттерна — навык, который отличает хорошего разработчика от начинающего.