ООП

ООП #

1. Что такое ООП? #

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

  • Полей
  • Конструктора
  • Методов


2. Плюсы и минусы ООП #

Плюсы:

  • Делим программу на «модули»-классы, ОБЪЕКТЫ каждый из которых делает свою часть работы.
  • Код можно повторно использовать в любом месте программы, это экономит время (не нужно писать однотипные функции для разных сущностей).
  • «Более естественная» декомпозиция ПО существенно облегчает его разработку (код легко читается и быстро пишется).
  • Возможность создавать расширяемые системы (extensible systems), именно это отличает ООП от традиционных методов программирования.

Минусы:

  • Снижение производительности, увеличение потребности памяти (Информация распределяется на множество мелких инкапсулированных объектов ⇒ на них растет количество ссылок)


3. Принципы ООП (наследование, инкапсуляция, полиморфизм, абстракция) #

Инкапсуляция - объединение данных и методов, работающих с ними в одном классе, а так же сокрытие деталей реализации от пользователя. В Python инкапсуляция реализуется через соглашения об именовании (один подчеркивание _ для защищенных членов, два подчеркивания __ для приватных) и свойства (property декораторы).

Наследование - возможность порождать один класс от другого (родительского) с сохранением всех его свойств и методов, добавляя при необходимости новые свойства и методы. Python поддерживает множественное наследование.

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

  • Динамическим (duck typing, переопределение методов)
    • Достигается переопределением методов в подклассах
    • Решение, какую версию метода вызвать, принимается во время выполнения программы
    • В Python используется принцип “утиной типизации” - важен не тип объекта, а наличие у него нужного метода
class Animal:
    def sound(self):
        print("Some generic animal sound")

class Dog(Animal):
    def sound(self):  # Переопределение метода
        print("Woof!")

class Cat(Animal):
    def sound(self):  # Переопределение метода
        print("Meow!")

# Демонстрация
def make_sound(animal):
    animal.sound()  # Полиморфный вызов

a1 = Dog()
a2 = Cat()

make_sound(a1)  # Woof!
make_sound(a2)  # Meow!

# Утиная типизация - даже не обязательно наследоваться от Animal
class Car:
    def sound(self):
        print("Vroom!")

make_sound(Car())  # Vroom!

Переменная может ссылаться на объект Dog, Cat или даже Car, а метод sound() будет вызван в соответствии с реальным типом объекта в рантайме.

  • Статическим (перегрузка функций)
    • В Python отсутствует традиционная перегрузка методов как в Java
    • Достигается через параметры по умолчанию, *args, **kwargs или с помощью декораторов (например, @singledispatch)
from functools import singledispatch

# Способ 1: Параметры по умолчанию
class MathUtils:
    def add(self, a, b, c=None):
        if c is not None:
            return a + b + c
        return a + b

# Способ 2: singledispatch для перегрузки по типам
@singledispatch
def process(value):
    return f"Обработка значения: {value}"

@process.register(int)
def _(value):
    return f"Обработка целого числа: {value}"

@process.register(str)
def _(value):
    return f"Обработка строки: {value}"

@process.register(list)
def _(value):
    return f"Обработка списка из {len(value)} элементов"

# Демонстрация
utils = MathUtils()
print(utils.add(2, 3))        # 5
print(utils.add(2, 3, 4))      # 9

print(process(42))             # Обработка целого числа: 42
print(process("hello"))        # Обработка строки: hello
print(process([1, 2, 3]))      # Обработка списка из 3 элементов

Здесь метод add имеет одно имя, но может принимать разное количество аргументов благодаря параметру по умолчанию, а декоратор @singledispatch позволяет выбрать реализацию на основе типа аргумента.

Абстракция – это способ выделить набор общих характеристик объекта, исключая из рассмотрения частные и незначимые. Соответственно, абстракция – это набор всех таких характеристик. В Python абстракция реализуется через абстрактные базовые классы (ABC) из модуля abc.

from abc import ABC, abstractmethod

class Shape(ABC):  # Абстрактный класс
    @abstractmethod
    def area(self):
        """Абстрактный метод - должен быть реализован в подклассах"""
        pass
    
    @abstractmethod
    def perimeter(self):
        """Абстрактный метод"""
        pass
    
    def description(self):  # Обычный метод
        return f"Это фигура с площадью {self.area()}"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):  # Реализация абстрактного метода
        return self.width * self.height
    
    def perimeter(self):  # Реализация абстрактного метода
        return 2 * (self.width + self.height)

# Использование
rect = Rectangle(5, 3)
print(rect.area())        # 15
print(rect.perimeter())   # 16
print(rect.description()) # Это фигура с площадью 15


4. Класс, объект, интерфейс #

Класс – это описание ещё не созданного объекта, общий шаблон. Шаблон состоит из:

  • Атрибутов (полей) – переменных, принадлежащих объекту (имя, возраст для человека и т.д.). Определяют состояние объекта.
  • Конструктора (__init__) – специального метода, который автоматически вызывается при создании объекта и инициализирует его начальное состояние.
  • Методов – функций, принадлежащих классу, которые определяют поведение объекта (что он умеет делать).
class Person:
    # Конструктор - инициализация объекта
    def __init__(self, name, age):
        # Атрибуты (поля) объекта
        self.name = name
        self.age = age
    
    # Метод класса
    def introduce(self):
        return f"Привет, меня зовут {self.name}, мне {self.age} лет"
    
    # Ещё один метод
    def have_birthday(self):
        self.age += 1
        print(f"С днём рождения! Теперь {self.name} {self.age} лет")

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

# Создание объектов (экземпляров класса)
person1 = Person("Анна", 25)  # Вызов конструктора
person2 = Person("Иван", 30)

# Каждый объект имеет своё состояние
print(person1.name)  # Анна
print(person2.name)  # Иван

# Вызов методов объектов
print(person1.introduce())  # Привет, меня зовут Анна, мне 25 лет
person2.have_birthday()     # С днём рождения! Теперь Иван 31 лет

Интерфейс — в Python нет строгого понятия интерфейса как в Java, но аналогичная функциональность достигается через:

  • Абстрактные базовые классы (ABC) – определяют контракт (набор методов), который должен реализовать любой класс-наследник
  • Протоколы (Protocol) – неформальные интерфейсы, основанные на утиной типизации (начиная с Python 3.8)
from abc import ABC, abstractmethod
from typing import Protocol

# Способ 1: Абстрактный базовый класс (формальный интерфейс)
class Drawable(ABC):
    @abstractmethod
    def draw(self):
        """Абстрактный метод - должен быть реализован"""
        pass
    
    @abstractmethod
    def get_size(self):
        """Ещё один обязательный метод"""
        pass

# Класс, реализующий интерфейс Drawable
class Circle(Drawable):
    def __init__(self, radius):
        self.radius = radius
    
    def draw(self):  # Обязательная реализация
        print(f"Рисуем круг радиусом {self.radius}")
    
    def get_size(self):  # Обязательная реализация
        return f"Площадь круга: {3.14 * self.radius ** 2}"

# Способ 2: Протокол (утиная типизация)
class Flyable(Protocol):
    def fly(self) -> str:
        """Контракт: любой объект с методом fly() считается Flyable"""
        ...

class Bird:
    def fly(self):
        return "Птица летит"

class Airplane:
    def fly(self):
        return "Самолет летит"
    
    def refuel(self):
        print("Заправка")

# Функция, работающая с любым объектом, реализующим протокол Flyable
def make_it_fly(flying_obj: Flyable):
    print(flying_obj.fly())

# Использование
circle = Circle(5)
circle.draw()           # Рисуем круг радиусом 5
print(circle.get_size()) # Площадь круга: 78.5

bird = Bird()
plane = Airplane()

make_it_fly(bird)  # Птица летит
make_it_fly(plane) # Самолет летит - работает, даже если класс не наследует Flyable

Интерфейсы позволяют задавать общие действия, которые могут выполнять различные классы, не завися от их конкретной реализации. В Python это достигается как через явное наследование от ABC, так и через неявные протоколы (утиную типизацию).


5. Ассоциация, агрегация, композиция #

Ассоциация означает, что объекты двух классов могут ссылаться один на другой, иметь связь друг с другом. Один класс включает в себя другой класс в качестве одного из полей (атрибутов).

class Student:
    def __init__(self, name):
        self.name = name
        self.courses = []  # Ассоциация: студент связан с курсами
    
    def enroll(self, course):
        self.courses.append(course)
        course.add_student(self)  # Двунаправленная связь

class Course:
    def __init__(self, title):
        self.title = title
        self.students = []  # Ассоциация: курс связан со студентами
    
    def add_student(self, student):
        self.students.append(student)

# Использование
student = Student("Анна")
course = Course("Python")

student.enroll(course)  # Устанавливаем связь между объектами
print(f"{student.name} записана на {course.title}")
print(f"На курс {course.title} записано {len(course.students)} студентов")

Агрегация и композиция являются частными случаями ассоциации. Это более конкретизированные отношения между объектами, описывающие жизненный цикл и владение объектами.

Композиция — это более жёсткое отношение, когда объект не только является частью другого объекта, но и вообще не может принадлежать кому-то другому. Время жизни части полностью управляется целым (если целое уничтожается, части тоже уничтожаются).

class Page:
    def __init__(self, number, content):
        self.number = number
        self.content = content
    
    def read(self):
        return f"Страница {self.number}: {self.content}"

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        # Композиция: страницы создаются внутри книги и принадлежат только ей
        self.pages = [
            Page(1, "Введение в Python"),
            Page(2, "Основы синтаксиса"),
            Page(3, "ООП в Python")
        ]
    
    def read_page(self, page_number):
        if 1 <= page_number <= len(self.pages):
            return self.pages[page_number - 1].read()
        return "Страница не найдена"
    
    def __del__(self):
        print(f"Книга '{self.title}' удаляется, все её страницы тоже удаляются")

# Использование
book = Book("Изучаем Python", "Иван Петров")
print(book.read_page(1))  # Страница 1: Введение в Python

# При удалении книги страницы тоже "исчезают" - они не могут существовать отдельно
del book
# Страницы больше не доступны

Агрегация — отношение, когда один объект является частью другого, но при этом может существовать независимо и принадлежать разным объектам в разное время.

class Engine:
    def __init__(self, engine_type, horsepower):
        self.engine_type = engine_type
        self.horsepower = horsepower
        self.car = None  # Ссылка на текущую машину
    
    def install_in(self, car):
        """Установить двигатель в машину"""
        if self.car:
            print(f"Двигатель уже установлен в {self.car.model}")
        else:
            self.car = car
            print(f"Двигатель {self.engine_type} установлен в {car.model}")
    
    def remove_from_car(self):
        """Снять двигатель с машины"""
        if self.car:
            print(f"Двигатель снят с {self.car.model}")
            self.car = None
        else:
            print("Двигатель не установлен")
    
    def __str__(self):
        return f"Двигатель {self.engine_type} ({self.horsepower} л.с.)"

class Car:
    def __init__(self, model, color):
        self.model = model
        self.color = color
        self.engine = None  # Агрегация: двигатель может быть, а может и не быть
    
    def install_engine(self, engine):
        """Установить двигатель в эту машину"""
        if self.engine:
            print(f"В машине {self.model} уже есть двигатель")
        else:
            engine.install_in(self)  # Двигатель сам себя устанавливает
            self.engine = engine
    
    def remove_engine(self):
        """Снять двигатель с машины"""
        if self.engine:
            self.engine.remove_from_car()
            self.engine = None
        else:
            print(f"В машине {self.model} нет двигателя")
    
    def __str__(self):
        if self.engine:
            return f"{self.color} {self.model} с {self.engine}"
        else:
            return f"{self.color} {self.model} (без двигателя)"

# Демонстрация агрегации
# Создаём двигатели - они могут существовать независимо
v8_engine = Engine("V8", 400)
electric_engine = Engine("Электрический", 200)

# Создаём машины
car1 = Car("Ford Mustang", "Красный")
car2 = Car("Tesla Model S", "Белый")

print("--- Агрегация: двигатели могут переставляться между машинами ---")
print(v8_engine)  # Двигатель V8 (400 л.с.)
print(car1)       # Красный Ford Mustang (без двигателя)

# Устанавливаем двигатель в первую машину
print("\n--- Установка двигателя ---")
car1.install_engine(v8_engine)
print(car1)  # Красный Ford Mustang с Двигатель V8 (400 л.с.)

# Пытаемся установить тот же двигатель во вторую машину
print("\n--- Попытка установить занятый двигатель ---")
car2.install_engine(v8_engine)  # Двигатель уже занят

# Снимаем двигатель с первой машины и ставим во вторую
print("\n--- Перестановка двигателя ---")
car1.remove_engine()
car2.install_engine(v8_engine)
print(car1)  # Красный Ford Mustang (без двигателя)
print(car2)  # Белый Tesla Model S с Двигатель V8 (400 л.с.)

# Двигатель продолжает существовать, даже если все машины удалены
print("\n--- Двигатель живёт отдельно от машин ---")
del car1
del car2
print(v8_engine)  # Двигатель V8 (400 л.с.) всё ещё существует

Ключевые различия в Python:

  1. Композиция: объект-часть создаётся внутри конструктора и не может быть передан извне или использован отдельно
  2. Агрегация: объект-часть создаётся отдельно и передаётся в конструктор или метод, может переходить от одного объекта к другому
# Наглядное сравнение
class Wheel:
    def __init__(self, size):
        self.size = size

# Композиция - колёса создаются вместе с машиной
class CarWithComposition:
    def __init__(self, model):
        self.model = model
        # Колёса создаются внутри и привязаны к этой машине
        self.wheels = [Wheel(17) for _ in range(4)]
    
    def __del__(self):
        print(f"Машина {self.model} уничтожена, колёса тоже")

# Агрегация - колёса могут существовать отдельно
class CarWithAggregation:
    def __init__(self, model, wheels):
        self.model = model
        self.wheels = wheels  # Колёса приходят извне
    
    def __del__(self):
        print(f"Машина {self.model} уничтожена, но колёса остались")

# Демонстрация
print("=== Композиция ===")
car1 = CarWithComposition("BMW")
del car1  # Колёса уничтожаются вместе с машиной

print("\n=== Агрегация ===")
wheels = [Wheel(18) for _ in range(4)]
car2 = CarWithAggregation("Audi", wheels)
del car2  # Машина уничтожена, но колёса продолжают существовать
print(wheels)  # Колёса всё ещё здесь, их можно использовать в другой машине


6. Статическое и динамическое связывание #

Связывание (Binding) — процесс, в ходе которого программа определяет, какой метод или атрибут должен быть вызван или использован. В Python связывание имеет свои особенности, отличные от Java.

Раннее (статическое) связывание в Python #

В Python раннее связывание происходит во время определения класса или компиляции в байт-код. Применяется для:

  • Атрибутов класса (переменных уровня класса)
  • Статических методов (@staticmethod)
  • Методов класса (@classmethod)
  • Имен функций и переменных в области видимости
class MathOperations:
    # Атрибут класса - раннее связывание
    pi = 3.14159
    
    def __init__(self, value):
        self.value = value  # Атрибут экземпляра - позднее связывание
    
    @staticmethod
    def add(a, b):  # Статический метод - раннее связывание
        return a + b
    
    @classmethod
    def create_default(cls):  # Метод класса - раннее связывание
        return cls(0)
    
    def multiply(self, factor):  # Обычный метод - позднее связывание
        return self.value * factor

# Демонстрация раннего связывания
print(MathOperations.pi)  # 3.14159 - атрибут класса известен до создания объекта
print(MathOperations.add(5, 3))  # 8 - статический метод вызывается без объекта

# Создание объекта через метод класса
obj = MathOperations.create_default()  # Метод класса связан на этапе определения
print(obj.value)  # 0

Позднее (динамическое) связывание в Python #

В Python позднее связывание происходит во время выполнения программы. Используется для:

  • Методов экземпляра (обычные методы)
  • Переопределенных методов (полиморфизм)
  • Атрибутов, добавляемых динамически
  • Утиной типизации
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):  # Будет связано динамически
        return f"{self.name} издаёт звук"

class Dog(Animal):
    def speak(self):  # Переопределение - динамическое связывание
        return f"{self.name} говорит: Гав!"

class Cat(Animal):
    def speak(self):  # Переопределение - динамическое связывание
        return f"{self.name} говорит: Мяу!"

# Демонстрация динамического связывания
animals = [
    Dog("Бобик"),
    Cat("Мурка"),
    Animal("Животное")
]

for animal in animals:
    # Какой метод speak() будет вызван?
    # Решение принимается ВО ВРЕМЯ ВЫПОЛНЕНИЯ на основе типа объекта
    print(animal.speak())
# Вывод:
# Бобик говорит: Гав!
# Мурка говорит: Мяу!
# Животное издаёт звук

Динамическое связывание атрибутов в Python #

Уникальная особенность Python - возможность динамически добавлять атрибуты и методы:

class DynamicClass:
    def __init__(self, name):
        self.name = name  # Обычный атрибут

# Создаем объект
obj = DynamicClass("Объект1")

# Динамически добавляем новый атрибут (позднее связывание)
obj.new_attr = "Я появился во время выполнения!"
print(obj.new_attr)  # Я появился во время выполнения!

# Динамически добавляем метод
def new_method(self):
    return f"Динамический метод для {self.name}"

# Привязываем метод к экземпляру
obj.dynamic_method = new_method.__get__(obj)
print(obj.dynamic_method())  # Динамический метод для Объект1

# Добавляем метод к классу (будет доступен всем экземплярам)
DynamicClass.class_method = lambda self: f"Метод класса для {self.name}"
print(obj.class_method())  # Метод класса для Объект1

Связывание в замыканиях и декораторах #

Позднее связывание в Python также проявляется в замыканиях:

# Проблема позднего связывания в замыканиях
functions = []
for i in range(3):
    def func():
        return i  # i будет связано ПОЗДНЕ, при вызове функции
    functions.append(func)

# Кажется, что функции должны вернуть 0, 1, 2
for f in functions:
    print(f())  # Выводит: 2, 2, 2 (значение i после завершения цикла)

# Решение - раннее связывание через аргумент по умолчанию
functions_fixed = []
for i in range(3):
    def func(x=i):  # i связывается РАННЕ, в момент определения
        return x
    functions_fixed.append(func)

for f in functions_fixed:
    print(f())  # Выводит: 0, 1, 2

Связывание в декораторах и property #

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property  # Дескриптор - позднее связывание при доступе
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9
    
    @staticmethod
    def from_fahrenheit(value):  # Статический метод - раннее связывание
        celsius = (value - 32) * 5/9
        return Temperature(celsius)

# Использование
t = Temperature(25)
print(t.fahrenheit)  # 77.0 - свойство вычисляется динамически

t.fahrenheit = 100
print(t._celsius)  # 37.777... - сеттер сработал динамически

# Статический метод вызван через класс (раннее связывание)
t2 = Temperature.from_fahrenheit(212)
print(t2._celsius)  # 100.0

Ключевые отличия от Java #

АспектJavaPython
Раннее связываниеprivate, final, static, перегруженные методыСтатические методы, методы класса, атрибуты класса
Позднее связываниеПереопределенные методы, абстрактные методыВсе методы экземпляра, переопределенные методы
Динамические атрибутыНет возможностиМожно добавлять в любой момент
Перегрузка методовНа этапе компиляцииНе поддерживается (используются args/kwargs)
# Практический пример с разными типами связывания
class PaymentSystem:
    tax_rate = 0.2  # Атрибут класса - раннее связывание
    
    def __init__(self, amount):
        self.amount = amount  # Атрибут экземпляра - позднее
    
    @classmethod
    def set_tax_rate(cls, rate):  # Метод класса - раннее связывание
        cls.tax_rate = rate
    
    @staticmethod
    def validate_amount(amount):  # Статический метод - раннее связывание
        return amount > 0
    
    def calculate_total(self):  # Метод экземпляра - позднее связывание
        return self.amount * (1 + self.tax_rate)

# Демонстрация
print(PaymentSystem.validate_amount(100))  # True - статический метод

PaymentSystem.set_tax_rate(0.25)  # Меняем атрибут класса
payment = PaymentSystem(1000)

# Динамическое связывание - метод определяется во время выполнения
print(payment.calculate_total())  # 1250.0

# Можно даже динамически заменить метод!
def new_calculation(self):
    return f"Новый расчёт: {self.amount * (1 + PaymentSystem.tax_rate)}"

payment.calculate_total = new_calculation.__get__(payment)
print(payment.calculate_total())  # Новый расчёт: 1250.0


7. Является – «is a», имеет – «has a» #

Наследование «is a» ЯВЛЯТЬСЯ (обобщение/расширение) – очень мощная связь

Ассоциация «has a» ИМЕТЬ (объекты ИМЕЮТ ссылки/ссылаются друг на друга)


8. Какая модель наследования используется в Python и какие её формы? | Как работает MRO? #

Hello World


9. Что можешь рассказать про принцип программирования SOLID? #

Hello World


10. Почему SOLID не всегда соблюдают в реальных проектах? #

Hello World


11. Что такое миксин (mixin) и для чего он применяется в объектно-ориентированном программировании? #

Hello World


12. Может ли абстрактный класс содержать реализацию методов? #

Hello World