ООП в Python - объектно-ориентированное программирование
Четыре столпа, на которых держится половина всего написанного кода - и половина всех вопросов на собеседовании
Содержание
Четыре принципа
ООП (объектно-ориентированное программирование) - это подход к написанию кода, при котором программа строится из объектов. У каждого объекта есть данные (атрибуты) и поведение (методы).
Четыре основных принципа ООП:
- Инкапсуляция - данные и методы упакованы внутри класса, доступ снаружи ограничен
- Наследование - класс может перенять свойства и методы другого класса
- Полиморфизм - разные классы могут реализовывать один и тот же метод по-своему
- Абстракция - снаружи виден только интерфейс, детали реализации скрыты
ООП - это парадигма программирования, где код организован вокруг объектов, а не функций и процедур.
Объект - это сущность с состоянием и поведением. Четыре принципа:
Инкапсуляция - данные защищены внутри класса и не торчат наружу;
Наследование - один класс берёт поведение другого и дополняет его;
Полиморфизм - разные объекты отвечают на один и тот же вызов по-разному; Абстракция - пользователь класса работает с интерфейсом и не знает, что происходит внутри. В Python всё является объектом - числа, строки, функции, сами классы.
Простыми словами
Представь, что ты описываешь автомобили.
У любого автомобиля есть характеристики - марка, цвет, скорость. И есть действия, которые он умеет делать - ехать, тормозить, сигналить. В ООП характеристики называются атрибутами, а действия - методами.
Чтобы не описывать каждый автомобиль с нуля, придумали класс - это шаблон, чертёж. Сам класс - это ещё не автомобиль, это описание того, как автомобиль устроен. А объект - это конкретный автомобиль, созданный по этому чертежу.
Класс → как чертёж автомобиля
Объект → конкретная машина, сделанная по чертежу
Из одного класса можно создать сколько угодно объектов - каждый со своими значениями атрибутов.
Теперь разберём каждый принцип так же просто.
Инкапсуляция - данные спрятаны внутри
Представь банкомат. Ты вставляешь карту и нажимаешь кнопки. Что происходит внутри - ты не знаешь и знать не должен. Тебе доступны только кнопки (интерфейс), а провода и моторчики скрыты (реализация).
В коде это значит: данные объекта не должны быть доступны всем подряд напрямую. Для чтения и записи используются специальные методы.
Наследование - берём готовое и дополняем
Представь, что есть класс Транспорт - у него есть марка, скорость и метод «ехать». Автомобиль - это транспорт, но с дополнением: у него есть багажник и метод «сигналить». Мотоцикл - тоже транспорт, но с коляской.
Вместо того чтобы писать Автомобиль и Мотоцикл с нуля, они наследуют всё от Транспорт и добавляют своё.
Полиморфизм - один вызов, разное поведение
Ты говоришь всем животным: «Говори!». Кошка мяукает, собака лает, корова мычит. Команда одна, а результат у каждого свой.
В коде это значит: разные классы реализуют один и тот же метод, но по-своему. Код, который вызывает этот метод, не знает с каким именно объектом работает - и это хорошо.
Абстракция - скрываем лишнее
Ты умеешь водить автомобиль. Нажимаешь педаль газа - машина едет. Тебе не нужно знать как работает двигатель внутреннего сгорания, чтобы доехать до магазина.
В коде абстракция означает: показываем снаружи только то, что нужно пользователю класса. Всё остальное - детали реализации - спрятано.
Код
Класс и объект
# Класс - это чертёж
class Car:
def __init__(self, brand, color, speed):
self.brand = brand # атрибут
self.color = color # атрибут
self.speed = speed # атрибут
def drive(self): # метод
return f"{self.brand} едет со скоростью {self.speed} км/ч"
def honk(self): # метод
return "Биип!"
# Объекты - конкретные машины по одному чертежу
car1 = Car("Toyota", "красная", 120)
car2 = Car("BMW", "чёрная", 200)
print(car1.drive()) # Toyota едет со скоростью 120 км/ч
print(car2.honk()) # Биип!
Инкапсуляция
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # двойной _ делает атрибут приватным
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
print("Недостаточно средств")
def get_balance(self): # единственный способ узнать баланс
return self.__balance
account = BankAccount("Ники", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance()) # 1300
# Напрямую не получится
print(account.__balance) # AttributeError
Наследование
# Базовый класс
class Transport:
def __init__(self, brand, speed):
self.brand = brand
self.speed = speed
def move(self):
return f"{self.brand} движется"
# Подкласс наследует всё от Transport и добавляет своё
class Car(Transport):
def __init__(self, brand, speed, trunk_volume):
super().__init__(brand, speed) # вызываем __init__ родителя
self.trunk_volume = trunk_volume # свой атрибут
def honk(self): # свой метод
return "Биип!"
class Motorcycle(Transport):
def wheelie(self):
return f"{self.brand} делает вилли!"
car = Car("Toyota", 120, 500)
moto = Motorcycle("Honda", 180)
print(car.move()) # Toyota движется (унаследовано)
print(car.honk()) # Биип! (своё)
print(moto.move()) # Honda движется (унаследовано)
print(moto.wheelie()) # Honda делает вилли!
Полиморфизм
class Animal:
def speak(self):
raise NotImplementedError
class Cat(Animal):
def speak(self):
return "Мяу!"
class Dog(Animal):
def speak(self):
return "Гав!"
class Cow(Animal):
def speak(self):
return "Муу!"
# Один цикл - разное поведение у каждого объекта
animals = [Cat(), Dog(), Cow()]
for animal in animals:
print(animal.speak())
# Мяу!
# Гав!
# Муу!
Абстракция
from abc import ABC, abstractmethod
# Абстрактный класс - нельзя создать объект напрямую
class Payment(ABC):
@abstractmethod
def pay(self, amount): # обязательно реализовать в подклассе
...
@abstractmethod
def refund(self, amount):
...
class CardPayment(Payment):
def pay(self, amount):
return f"Оплачено картой: {amount} руб."
def refund(self, amount):
return f"Возврат на карту: {amount} руб."
class CryptoPayment(Payment):
def pay(self, amount):
return f"Оплачено криптой: {amount} руб."
def refund(self, amount):
return f"Возврат в крипте: {amount} руб."
# Код работает с любым способом оплаты одинаково
def process_payment(payment: Payment, amount: int):
print(payment.pay(amount))
process_payment(CardPayment(), 1000) # Оплачено картой: 1000 руб.
process_payment(CryptoPayment(), 1000) # Оплачено криптой: 1000 руб.
Глубже
В Python всё - объект. Число 42 - объект класса int. Строка "hello" - объект класса str. Функция - объект класса function. Даже сами классы - объекты класса type. Это не метафора, это буквально так: у каждого из них есть атрибуты и методы.
print(type(42)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type(int)) # <class 'type'>
__init__ - это не конструктор. Точнее, не совсем конструктор. Настоящий конструктор в Python - это __new__, который создаёт объект в памяти. __init__ - это инициализатор, который заполняет уже созданный объект данными. В 99% случаев это различие не важно, но на вопросе «что такое __init__» лучше знать ответ.
self - это просто соглашение. Python передаёт ссылку на текущий объект первым аргументом в каждый метод. Переменную принято называть self, но это не ключевое слово - можно написать this или me. Просто не надо.
Множественное наследование. Python поддерживает наследование от нескольких классов одновременно. Порядок разрешения методов при этом определяется алгоритмом C3-линеаризации (MRO - Method Resolution Order). Посмотреть его можно через ClassName.__mro__. На собеседовании достаточно знать, что такое есть и что Python разрешает конфликты через MRO слева направо.
Утиная типизация. В Python необязательно использовать ABC для полиморфизма. Если у объекта есть нужный метод - он подойдёт, независимо от того, какого он класса. «Если это ходит как утка и крякает как утка - это утка». Это делает Python гибче строго типизированных языков, но требует аккуратности.