ООП #
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:
- Композиция: объект-часть создаётся внутри конструктора и не может быть передан извне или использован отдельно
- Агрегация: объект-часть создаётся отдельно и передаётся в конструктор или метод, может переходить от одного объекта к другому
# Наглядное сравнение
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 #
| Аспект | Java | Python |
|---|---|---|
| Раннее связывание | 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