ООП Общая теория

Содержание

  1. Python и объектно-ориентированный подход
  2. Что такое ООП
  3. Класс и объект
  4. Основные принципы ООП
  5. Когда ООП действительно полезно
  6. Когда ООП может быть лишним
  7. Частые ошибки
  8. Практика

Python и объектно-ориентированный подход

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

Это значит, что у значений есть не только данные, но и поведение.

text = "python"
print(text.upper())
 
numbers = [1, 2, 3]
numbers.append(4)
print(numbers)

text - объект строки, у которого есть метод upper(). numbers - объект списка, у которого есть метод append().


Что такое ООП

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

Кратко:

  • класс описывает шаблон;
  • объект создается по этому шаблону;
  • атрибуты хранят состояние объекта;
  • методы описывают поведение объекта.

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


Класс и объект

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

class Course:
    def __init__(self, title, duration_hours):
        self.title = title
        self.duration_hours = duration_hours
 
    def describe(self):
        return f"Курс {self.title}: {self.duration_hours} часов"
 
python_course = Course("Python для AQA", 48)
api_course = Course("API testing", 24)
 
print(python_course.describe())
print(api_course.describe())

Оба объекта созданы по одному классу Course, но хранят разные данные.


Основные принципы ООП

Инкапсуляция

Инкапсуляция - это объединение данных и методов внутри объекта, а также контроль доступа к внутреннему состоянию.

Идея: внешний код не должен случайно ломать внутренние данные объекта. Для этого данные можно скрыть и менять через методы или свойства. Инструменты для этого — защищённые атрибуты и @property — разберём в 03.06 - Методы классов.

class Product:
    def __init__(self, name, price):
        self.name = name
        self.__price = price
 
    @property
    def price(self):
        return self.__price
 
    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Цена не может быть отрицательной")
        self.__price = value
 
product = Product("Монитор", 18000)
product.price = 17000
print(product.price)

Внешний код работает с price, но проверка находится внутри класса.

Наследование

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

class User:
    def __init__(self, name):
        self.name = name
 
    def login(self):
        return f"{self.name} вошел в систему"
 
class Admin(User):
    def delete_record(self):
        return "Запись удалена"
 
admin = Admin("Ирина")
print(admin.login())
print(admin.delete_record())

Admin получил метод login() от User и добавил свое действие.

Полиморфизм

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

class EmailSender:
    def send(self, message):
        return f"Email отправлен: {message}"
 
class SmsSender:
    def send(self, message):
        return f"SMS отправлено: {message}"
 
def notify(sender, message):
    print(sender.send(message))
 
notify(EmailSender(), "Отчет готов")
notify(SmsSender(), "Код подтверждения: 4821")

Функция notify не знает деталей классов. Ей важно только, что у объекта есть метод send.

Абстракция

Абстракция скрывает детали реализации и оставляет только важный интерфейс. Пользователь объекта должен понимать, что можно сделать, но не обязан знать, как это устроено внутри.

В Python для строгой абстракции можно использовать модуль abc.

from abc import ABC, abstractmethod
 
class Report(ABC):
    @abstractmethod
    def generate(self):
        pass
 
class PdfReport(Report):
    def generate(self):
        return "PDF-отчет создан"
 
class CsvReport(Report):
    def generate(self):
        return "CSV-отчет создан"

Если класс наследуется от Report, он обязан реализовать метод generate().


Когда ООП действительно полезно

ООП хорошо подходит, когда в проекте есть много связанных сущностей и правил.

Примеры:

  • интернет-магазин: Product, Cart, Order, User;
  • система обучения: Student, Course, Lesson, Progress;
  • банковская система: Account, Transaction, Client;
  • тестовый фреймворк: ApiClient, BasePage, TestUser, ReportBuilder.

ООП помогает:

  • разделить проект на понятные части;
  • переиспользовать код;
  • расширять систему без переписывания всего проекта;
  • проще тестировать отдельные компоненты;
  • распределять работу между разработчиками.

Пример простой структуры магазина:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
 
class Cart:
    def __init__(self):
        self.items = []
 
    def add_product(self, product, quantity):
        self.items.append({"product": product, "quantity": quantity})
 
    def calculate_total(self):
        total = 0
        for item in self.items:
            total += item["product"].price * item["quantity"]
        return total
 
class Order:
    def __init__(self, cart):
        self.cart = cart
        self.status = "new"
 
    def complete(self):
        self.status = "completed"
        return f"Заказ оформлен. Сумма: {self.cart.calculate_total()} руб."
 
keyboard = Product("Клавиатура", 3500)
monitor = Product("Монитор", 18000)
 
cart = Cart()
cart.add_product(keyboard, 2)
cart.add_product(monitor, 1)
 
order = Order(cart)
print(order.complete())

Когда ООП может быть лишним

ООП не нужно использовать везде. Для маленьких задач классы могут только усложнить код.

ООП может быть избыточным, если:

  • нужно один раз обработать файл;
  • задача решается двумя-тремя функциями;
  • нет устойчивых сущностей с состоянием;
  • код не планируется расширять;
  • класс создается только ради одной функции.

Пример лишнего класса:

class MathOperations:
    def add(self, a, b):
        return a + b
 
math = MathOperations()
print(math.add(2, 3))

Проще так:

def add(a, b):
    return a + b
 
print(add(2, 3))

Главный критерий: ООП должно упрощать структуру, а не создавать лишнюю оболочку.


Частые ошибки

Избыточное использование классов

Ошибка: создавать класс для задачи, где достаточно функции.

Решение: сначала оценить, есть ли у сущности состояние, поведение и перспектива расширения.

Неправильное наследование

Ошибка: делать дочерний класс там, где между сущностями нет отношения является разновидностью.

class Document:
    pass
 
class Printer(Document):
    pass  # логически неверно: принтер не является документом

Лучше отделить сущности и передавать документ в методы принтера.

class Document:
    pass
 
class Printer:
    def print_document(self, document):
        print("Печать документа")

Отсутствие инкапсуляции

Ошибка: позволить внешнему коду менять важные данные как угодно.

class Account:
    def __init__(self):
        self.balance = 0
 
account = Account()
account.balance = -500

Лучше контролировать изменение через метод или setter.

class Account:
    def __init__(self):
        self.__balance = 0
 
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.__balance += amount
 
    def get_balance(self):
        return self.__balance

Слишком большие классы

Если класс делает все сразу, его трудно читать и тестировать. Лучше разделять ответственность: один класс - одна понятная роль.


Практика

  • Задание 1: определить сущности

    Для системы онлайн-курсов предложите классы и их ответственность.

    Минимальный набор:

    • Student;
    • Course;
    • Lesson;
    • ProgressTracker.

    Для каждого класса укажите 2-3 атрибута и 1-2 метода.

  • Задание 2: инкапсуляция

    Создайте класс Wallet.

    Требования:

    • приватный атрибут __balance;
    • метод deposit(amount);
    • метод withdraw(amount);
    • метод get_balance();
    • нельзя пополнять или снимать отрицательные суммы;
    • нельзя снять больше, чем есть на балансе.
  • Задание 3: наследование

    Создайте класс BaseUser с атрибутом name и методом get_info(). Создайте классы StudentUser и MentorUser, которые наследуются от BaseUser и добавляют свои методы.

    Проверьте, что дочерние объекты используют общий метод родителя.

  • Задание 4: полиморфизм

    Создайте классы PdfExporter, CsvExporter, JsonExporter. У каждого должен быть метод export(data). Напишите функцию run_export(exporter, data), которая вызывает export у переданного объекта.

  • Задание 5: абстракция

    Создайте абстрактный класс PaymentMethod с методом pay(amount). Затем создайте классы CardPayment и CashPayment, которые реализуют этот метод.

    Проверьте, что нельзя создать объект абстрактного класса напрямую.


Короткий итог

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


⬅️ Назад: 03.02 - Исключения и обработка ошибок в Python | Далее: 03.04 - ООП Классы ➡️ Модуль: 03 - MOC