$ sudo teach IT
МОДУЛЬ 1 · УРОК 2

Процедурный подход vs ООП: в чём реальная разница?

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

⏱ ~25 минут 🎓 Для новичков 🐍 Python 3

🧱 Что такое процедурный подход

Слово «процедурный» звучит страшновато, но за ним стоит очень простая идея. Процедурное программирование — это когда программа состоит из функций (их ещё называют процедурами), которые последовательно обрабатывают данные.

Вы уже наверняка писали что-то вроде этого: создали переменную, передали её в функцию, получили результат. Это и есть процедурный стиль. Он появился раньше ООП, и сегодня он никуда не делся — миллионы программ написаны именно так.

Суть процедурного подхода: данные и функции существуют отдельно. Функция получает данные через аргументы, что-то с ними делает и возвращает результат. Она не «помнит» ничего между вызовами — не хранит состояние.

Представьте мясорубку: вы подаёте мясо (данные), крутите ручку (вызываете функцию), получаете фарш (результат). Мясорубка не помнит, что вы закладывали в неё вчера. Каждый раз — с нуля, по тем данным, которые вы передали сейчас.

В Python процедурный стиль выглядит так: у вас есть словарь или набор переменных с данными, и есть функции, которые принимают эти данные первым аргументом. Данные и поведение — в разных местах кода.

🏦 Задача: банковский счёт

Чтобы сравнение было честным, возьмём конкретную задачу и решим её двумя способами. Задача простая и понятная: банковский счёт.

Нам нужно уметь:

🆕
Создать счёт

Завести новый счёт с именем владельца и начальным балансом

💰
Пополнить

Положить деньги на счёт

💸
Снять

Снять деньги, если хватает баланса

📋
Посмотреть баланс

Узнать текущий баланс счёта

Казалось бы, простая задача. Но именно на ней очень хорошо видна разница между двумя подходами. Поехали.

🔧 Решение 1: процедурный подход

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

Вот как это выглядит на практике:

# ===========================================
# ПРОЦЕДУРНЫЙ ПОДХОД: банковский счёт
# ===========================================

# Данные счёта хранятся в обычном словаре.
# Это просто набор ключей и значений — никакой логики внутри.

def create_account(owner, initial_balance=0):
    """Создать новый банковский счёт."""
    # Возвращаем словарь — это и есть наш "счёт"
    return {
        "owner": owner,
        "balance": initial_balance,
        "transactions": []  # история операций
    }


def deposit(account, amount):
    """Пополнить счёт на указанную сумму."""
    if amount <= 0:
        print("Ошибка: сумма пополнения должна быть больше нуля")
        return

    account["balance"] += amount
    account["transactions"].append(f"Пополнение: +{amount}")
    print(f"Счёт пополнен на {amount}. Новый баланс: {account['balance']}")


def withdraw(account, amount):
    """Снять деньги со счёта."""
    if amount <= 0:
        print("Ошибка: сумма снятия должна быть больше нуля")
        return

    if amount > account["balance"]:
        print(f"Ошибка: недостаточно средств. Баланс: {account['balance']}")
        return

    account["balance"] -= amount
    account["transactions"].append(f"Снятие: -{amount}")
    print(f"Снято {amount}. Новый баланс: {account['balance']}")


def get_balance(account):
    """Показать текущий баланс."""
    print(f"Баланс счёта {account['owner']}: {account['balance']}")
    return account["balance"]


def print_history(account):
    """Показать историю операций."""
    print(f"\n--- История счёта {account['owner']} ---")
    if not account["transactions"]:
        print("Операций пока нет")
    else:
        for transaction in account["transactions"]:
            print(transaction)
    print("---")


# ===========================================
# Используем наш код
# ===========================================

# Создаём два счёта — просто два словаря
alice_account = create_account("Алиса", 1000)
bob_account = create_account("Боб", 500)

# Работаем со счётом Алисы
deposit(alice_account, 500)       # Алиса кладёт 500
withdraw(alice_account, 200)      # Алиса снимает 200
get_balance(alice_account)        # Смотрим баланс

# Работаем со счётом Боба
deposit(bob_account, 300)         # Боб кладёт 300
withdraw(bob_account, 1000)       # Боб пытается снять 1000 — не хватит!
get_balance(bob_account)

# История операций
print_history(alice_account)
print_history(bob_account)

Посмотрите внимательно на этот код. Видите закономерность? Каждая функция принимает account первым аргументом. То есть мы всё время вручную «связываем» данные с функциями. Программист должен всегда помнить: «Это функция для работы со счётом, поэтому первым аргументом я обязан передать словарь счёта».

⚠️ Слабое место: ничто не мешает написать deposit(bob_account, amount), когда вы думали о счёте Алисы. Или передать в функцию счёта вообще не тот словарь — например, данные пользователя вместо данных счёта. Python не будет ругаться — он просто упадёт с непонятной ошибкой где-то глубже. Данные и поведение не скреплены — это и есть главная хрупкость.

✨ Решение 2: объектно-ориентированный подход

Теперь решим ту же задачу через ООП. Мы создадим класс — это как шаблон или чертёж, в котором данные и функции живут вместе. Функции внутри класса называются методами.

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

# ===========================================
# ООП ПОДХОД: банковский счёт
# ===========================================

class BankAccount:
    """
    Класс банковского счёта.
    Данные и методы работы с ними — всё в одном месте.
    """

    def __init__(self, owner, initial_balance=0):
        """
        Это специальный метод — конструктор.
        Он вызывается автоматически при создании нового счёта.
        self — это сам объект (конкретный экземпляр счёта).
        """
        self.owner = owner              # имя владельца
        self.balance = initial_balance  # текущий баланс
        self.transactions = []          # история операций

    def deposit(self, amount):
        """Пополнить счёт."""
        if amount <= 0:
            print("Ошибка: сумма пополнения должна быть больше нуля")
            return

        self.balance += amount
        self.transactions.append(f"Пополнение: +{amount}")
        print(f"Счёт пополнен на {amount}. Новый баланс: {self.balance}")

    def withdraw(self, amount):
        """Снять деньги."""
        if amount <= 0:
            print("Ошибка: сумма снятия должна быть больше нуля")
            return

        if amount > self.balance:
            print(f"Ошибка: недостаточно средств. Баланс: {self.balance}")
            return

        self.balance -= amount
        self.transactions.append(f"Снятие: -{amount}")
        print(f"Снято {amount}. Новый баланс: {self.balance}")

    def get_balance(self):
        """Показать текущий баланс."""
        print(f"Баланс счёта {self.owner}: {self.balance}")
        return self.balance

    def print_history(self):
        """Показать историю операций."""
        print(f"\n--- История счёта {self.owner} ---")
        if not self.transactions:
            print("Операций пока нет")
        else:
            for transaction in self.transactions:
                print(transaction)
        print("---")


# ===========================================
# Используем наш класс
# ===========================================

# Создаём объекты — экземпляры класса BankAccount
alice_account = BankAccount("Алиса", 1000)
bob_account = BankAccount("Боб", 500)

# Работаем через методы объекта
alice_account.deposit(500)        # Алиса кладёт 500
alice_account.withdraw(200)       # Алиса снимает 200
alice_account.get_balance()       # Смотрим баланс

# Счёт Боба — отдельный объект, ничего не перепутать
bob_account.deposit(300)
bob_account.withdraw(1000)        # Не хватит!
bob_account.get_balance()

# История
alice_account.print_history()
bob_account.print_history()

Обратите внимание: результат тот же самый — программа работает идентично. Но посмотрите на использование: теперь вместо deposit(alice_account, 500) мы пишем alice_account.deposit(500). Счёт сам знает, как с собой работать.

✅ Важное наблюдение: метод deposit принадлежит конкретному объекту. Вы физически не можете вызвать его «не на том счёте» — нужно явно написать alice_account.deposit(...) или bob_account.deposit(...). Python сам передаёт правильные данные (через self), и вам не нужно об этом думать.

💾 Ключевое понятие: состояние (state)

Мы подошли к одному из важнейших понятий, которое объясняет, почему ООП вообще появилось и в каких ситуациях оно особенно полезно.

Состояние (state) — это данные, которые меняются со временем и которые нужно «помнить» между операциями.

В нашем примере — баланс счёта и история транзакций. Когда Алиса кладёт деньги, баланс изменился. Когда позже снимает — баланс снова изменился. Между этими двумя событиями прошло время, но счёт «помнит», сколько на нём денег.

🧠 Аналогия из жизни

Представьте лампочку с диммером. У неё есть состояние — текущая яркость. Когда вы крутите ручку, яркость меняется, и она остаётся такой, пока вы снова не покрутите. Лампочка «помнит» своё текущее состояние.

Теперь функция без состояния — это как обычный выключатель: передали «включить» — включилась, передали «выключить» — выключилась. Никакой памяти о предыдущих значениях.

Объекты в ООП идеально подходят именно тогда, когда ваши данные имеют состояние — когда их нужно изменять и «помнить» эти изменения. Каждый объект BankAccount несёт в себе собственное состояние: свой баланс, своё имя владельца, свою историю. Это не смешивается с состоянием другого объекта.

Простое правило: если у вашей «сущности» есть данные, которые меняются со временем, и поведение, которое зависит от этих данных — это хороший кандидат для класса.

⚖️ Честный разбор: плюсы и минусы обоих подходов

Никакой подход не является «лучшим» сам по себе — у каждого есть своя ниша. Давайте разберём честно.

✅ Плюсы процедурного

  • Проще для понимания новичком
  • Меньше абстракций — код «читается прямо»
  • Отлично для скриптов и небольших задач
  • Быстрее писать, когда задача простая
  • Легко тестировать отдельные функции

❌ Минусы процедурного

  • При росте кода — хаос с данными
  • Данные и логика разделены — можно перепутать
  • Сложно масштабировать
  • Трудно переиспользовать куски кода
  • Нет защиты данных от случайного изменения

✅ Плюсы ООП

  • Данные и поведение вместе — меньше ошибок
  • Инкапсуляция: данные защищены внутри объекта
  • Легко создавать много похожих объектов
  • Хорошо масштабируется на большие проекты
  • Переиспользование через наследование

⚠️ Минусы ООП

  • Сложнее в освоении для новичков
  • Больше кода для простых задач
  • Можно «переусложнить» простую программу
  • Иногда скрывает то, что реально происходит

🎯 Когда что выбирать — честный ответ

Один из самых частых вопросов новичков: «Ладно, я понял разницу — но что мне использовать в своём проекте?». Давайте разберём по реальным ситуациям.

🔧 Используй процедурный подход, когда:

  • Пишешь скрипт для автоматизации — переименовать файлы, обработать CSV, отправить уведомление
  • Задача небольшая и не будет расти
  • Нет «сущностей» с состоянием — просто берёшь данные, обрабатываешь, отдаёшь
  • Пишешь утилиты и вспомогательные функции (это нормально даже в ООП-проектах)

🏗️ Используй ООП, когда:

  • Есть сущности с состоянием: пользователь, товар, заказ, сессия, соединение с БД
  • Нужно создавать много похожих объектов: 1000 пользователей, 500 товаров
  • Проект будет расти и развиваться — команда, версии, новые фичи
  • Пишешь фреймворк, библиотеку или сложное приложение
  • Нужно наследование — например, разные типы пользователей (Admin, Editor, Viewer)

💡 Главный секрет

В реальных проектах эти два подхода не противоречат друг другу — они прекрасно уживаются вместе. Django (популярный Python-фреймворк для веба) написан на ООП: модели данных — это классы. Но внутри этих классов часто используются обычные функции. А утилитарные скрипты рядом с Django-проектом запросто могут быть процедурными. Главное — выбирать инструмент под задачу, а не «быть верным одной школе».

Опытные разработчики не думают категориями «процедурный vs ООП» — они думают: «Какая структура сделает этот код понятнее и удобнее в поддержке?» И иногда это класс, иногда — просто функция.

🔒 Мини-введение: что такое инкапсуляция

Раз уж мы сравниваем подходы, стоит ввести один термин, который будет встречаться в курсе постоянно. Это инкапсуляция — одна из трёх ключевых идей ООП.

Инкапсуляция (от слова «капсула») — это принцип, по которому данные и методы работы с ними упаковываются вместе и скрываются от внешнего мира. Снаружи видно только то, что нужно — остальное спрятано внутри.

📦
Данные внутри

Поля объекта хранятся внутри него — посторонний код не обязан знать, как устроены внутренности

🚪
Интерфейс снаружи

Методы — это «двери», через которые можно взаимодействовать с объектом. Проверки и логика — внутри

🛡️
Защита данных

Нельзя «случайно» установить баланс в -100500 в обход метода withdraw — логика защищает от таких ошибок

В нашем примере метод withdraw проверяет, достаточно ли денег — и только тогда изменяет баланс. В процедурном коде такую же защиту можно написать, но ничто не мешает другому программисту изменить account["balance"] напрямую, минуя проверку. В ООП мы можем это ограничить — но об этом подробнее в одном из следующих уроков.

✅ Итоги урока

  • Процедурный подход — функции + отдельные данные, передаваемые через аргументы
  • ООП — данные и функции (методы) объединены в объект, который несёт своё состояние
  • Состояние (state) — это данные объекта, которые меняются со временем и «помнятся» между операциями
  • ООП особенно силён, когда есть сущности с состоянием и когда проект масштабируется
  • Процедурный подход проще и быстрее для маленьких задач без «сущностей»
  • Инкапсуляция — принцип объединения данных и методов в одну «капсулу» с защитой от внешнего вмешательства
  • В реальных проектах оба подхода сочетаются — главное выбирать правильный инструмент под задачу
В следующем уроке мы напишем первый класс с нуля, разберём конструктор __init__, поймём, что такое self, и создадим наши первые настоящие объекты.