OOP
ООП⚑
Що таке ООП? Яка відмінність від процедурного?⚑
Об'єктно-орієнтоване програмування (ООП) - це парадигма програмування, яка базується на концепції "об'єктів". Основними принципами ООП є ідея поділу програми на об'єкти, які взаємодіють один з одним, та спрощення розробки та обслуговування програм завдяки модульності і розподіленій відповідальності. ООП корисний тоді, коли у об'єктів є стан.
Кожен об'єкт групує заданий набір інформації (властивостей) та дій (методів), які може виконувати цей об'єкт.
Основні поняття ООП включають
- Класи і об'єкти (Classes and Objects): Клас - це шаблон або опис об'єкта, який визначає його властивості та методи. Об'єкт - це конкретний екземпляр класу, який може мати свої унікальні значення властивостей.
- Інкапсуляція (Encapsulation): Інкапсуляція визначає, що властивості та методи об'єкта повинні бути збережені разом та захищені від зовнішнього доступу. Це допомагає управляти доступом до даних і методів.
- Наслідування (Inheritance): Наслідування дозволяє створювати новий клас на основі існуючого класу, наслідуючи його властивості та методи. Це спрощує створення нових класів та перевикористання коду.
- Поліморфізм (Polymorphism): Поліморфізм дозволяє об'єктам різних класів використовувати однаковий інтерфейс для виконання різних операцій. Це спрощує роботу з різнорідними об'єктами.
На відміну від ООП, в процедурному програмуванні код організований навколо послідовного виконання функцій. Функції отримують дані, обробляють їх та повертають результат.
Інкапсуляція⚑
Summary
Інкапсуляція - це один з основних принципів об'єктно-орієнтованого програмування (ООП), що дозволяє об'єднати дані та функції в один об'єкт і приховати їх внутрішню реалізацію від зовнішнього світу. Інкапсуляція - приховання реалізації, працюємо з API.
Тобто, інкапсуляція - це обмеження доступу до компонентів об'єкту (методів і змінних). Інкапсуляція робить деякі з компонентів доступними тільки з середини класу.
Інкапсуляція в Python працює тільки на рівні домовленості між програмістами про те, які атрибути являються загальнодоступними, а які - внутрішніми. У Python, інкапсуляцію зазвичай забезпечують за допомогою модифікаторів доступу, таких як public
, private
та protected
.
public
- це найбільш видимий рівень доступу, коли змінні і методи можуть бути доступними ззовні класу і за його межами без обмежень.private
- змінні та методи, які починаються з символу підкреслення (наприклад,_private_var
), вважаються приватними і не повинні бути використовувані ззовні класу. Такі змінні та методи доступні лише в середині класу або в методах класу.protected
- змінні та методи, які починаються з подвійного символу підкреслення (наприклад,__protected_var
), вважаються захищеними. Вони можуть бути доступні ззовні класу, але зазвичай не рекомендується їх використовувати безпосередньо. Замість цього, зазвичай, їх використовують для успадкування і внутрішньої реалізації класів.
Інкапсуляція дозволяє захистити внутрішню реалізацію класу і приховати змінні та методи, які не повинні бути доступними для зовнішнього використання. Це сприяє створенню більш чистого, безпечного та легкозмінного коду, оскільки змінні можуть змінюватися тільки зсередини класу, а зовнішні користувачі використовують лише визначені публічні методи.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # protected
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient funds!")
def get_balance(self):
return self.__balance
account = BankAccount(1000)
account.deposit(500) # use public methods to interact with account
account.withdraw(200)
print(account.get_balance()) # 1300
print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
Наслідування⚑
Summary
Наслідування - це один з ключових принципів ООП, який дозволяє створювати новий клас на основі вже існуючого класу (батьківського класу). Наслідування дозволяє використовувати властивості та методи батьківського класу в новому класі, що допомагає уникнути повторення коду та забезпечити використання вже існуючого функціоналу. Дочірній клас містить всі атрибути батьківського класу, при цьому деякі з них можуть бути перевизначені в дочірньому. Cтворення підкласу має супроводжуватися ідеєю спеціалізації - відношенням
is-a
(є).
У Python, наслідування здійснюється за допомогою вказання батьківського класу у визначенні нового класу в дужках після імені класу:
class ParentClass:
def parent_method(self):
print("This is a method from the parent class")
class ChildClass(ParentClass):
def child_method(self):
print("This is a method from the child class")
У цьому прикладі ChildClass
наслідує всі властивості та методи з ParentClass
. Це означає, що ChildClass
буде мати доступ до методу parent_method
, а також може мати свої власні методи, наприклад, child_method
.
При наслідуванні, клас, який успадковує властивості, називається підкласом або дочірнім класом, а клас, від якого успадковуються властивості, називається батьківським класом або суперкласом.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.name) # Output: Buddy
print(dog.speak()) # Output: Woof!
print(cat.name) # Output: Whiskers
print(cat.speak()) # Output: Meow!
Таким чином, наслідування дозволяє створювати ієрархії класів, де дочірні класи успадковують властивості та методи батьківських класів, а також можуть додавати свої власні методи та властивості.
Ми можемо отримати доступ до методів класу-предку або по прямому зверненню, або з допомогою функції super()
.
Хоча наслідування є потужною концепцією, воно має свої небезпеки. Основна з них полягає в тому, що кожного разу, коли розширювати базовий клас, створюється новий, який тісно пов’язаний з батьківським. Високе зв'язування (High Coupling) є однією з речей, яких потрібно уникати при розробці програмного забезпечення.
Хоча завжди повинні дбати про повторне використання коду, не варто змушувати дизайн використовувати успадкування для повторного використання коду лише тому, що отримуємо методи від батьківського класу безкоштовно. Правильний спосіб повторного використання коду полягає в тому, щоб мати дуже зчеплені (highly cohesive) об’єкти, які можна легко компонувати та працювати в багатьох контекстах.
Поліморфізм⚑
Summary
Поліморфізм - це один з ключових принципів ООП, який дозволяє об'єктам з різних класів використовувати один і той же інтерфейс, але мати різну реалізацію цього інтерфейсу. Іншими словами, це різна поведінка одного і того ж методу в різних класах. Тобто - один інтерфейс, але різна поведінка і різна реалізація.
Наприклад ми можемо додати два числа, і можемо додати два рядки. При цьому отримаємо різний результат - так як числа і рядки являються різними класами. Це реалізується з допомогою __add__
- "магічний методів" в Python, який використовується для перевантаження оператора додавання +
. Він дозволяє задати спеціальну реалізацію операції додавання для об'єктів певного класу.
__add__
для int
class MyInt:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other
num1 = MyInt(5)
num2 = 10
result = num1 + num2
print(result) # Output: 15
__add__
для str
class MyString:
def __init__(self, value):
self.value = value
def __add__(self, other):
return f"{self.value}{other}"
str1 = MyString("Hello, ")
str2 = "world!"
result = str1 + str2
print(result) # Output: Hello, world!
Абстракція⚑
Summary
Абстракція - відокремлення деталей з метою отримання можливості зосередитись на найважливіших особливостях об'єкту. Або це використання тільки тих характеристик об'єкту, які з достатньою точністю представляють його в системі.
У ООП, абстракція зазвичай досягається за допомогою створення абстрактних класів або інтерфейсів, які описують специфікацію поведінки об'єктів, але не надають конкретної реалізації. Абстрактні класи містять абстрактні методи, які мають бути перевизначені в дочірніх класах. Інтерфейси, у свою чергу, описують набір методів, які мають бути реалізовані в класах, які їх імплементують.
Простим прикладом абстракції може бути клас Shape
, який представляє геометричну фігуру. Абстрактний клас містить абстрактний метод area
, який об'явлений, але не має реалізації:
Дочірні класи, наприклад, Circle
та Rectangle
, успадковують Shape
і повинні перевизначити метод area
згідно своїх потреб:
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
За допомогою абстракції, ми можемо робити узагальнені операції над об'єктами типу Shape
, не знаючи конкретних деталей реалізації. Наприклад:
def print_area(shape):
print(f"The area of the shape is: {shape.area()}")
circle = Circle(5)
rectangle = Rectangle(4, 6)
print_area(circle) # Output: The area of the shape is: 78.5
print_area(rectangle) # Output: The area of the shape is: 24
Композиція проти Наслідування (Composition vs Inheritance)⚑
Summary
- Агрегація та композиція - це відношення
has-a
(має) між об'єктами, де один клас містить посилання на інший клас.- Наслідування - це відношення
is-a
(є), коли один клас є різновидом іншого. Наслідуваний клас отримує всі методи та властивості батьківського класу.- В загальному випадку, слід віддавати перевагу композиції перед наслідуванням, бо вона дає більше гнучкості.
Композиція - це підхід до створення програмного коду, при якому об'єкти одного класу включаються в інший клас як його атрибути, а не через наслідування. Це означає, що один клас містить посилання на об'єкти інших класів, і він може використовувати їх функціонал для досягнення певних завдань. Композиція дозволяє створювати складніші структури даних і об'єкти, комбінуючи простіші компоненти. Хороший код повторно використовується, маючи невеликі цілісні абстракції, а не створюючи ієрархії.
Відмінність від агрегації полягає в тому, що агрегація відображає "відношення частини до цілого", де один клас "складається" з інших класів, але ці частини можуть існувати окремо від цілого. Наприклад, автомобіль має двигун, але двигун може існувати сам по собі. Композиція також представляє "відношення частини до цілого", але тут частини не можуть існувати окремо від цілого. Якщо ціле знищується, то і частини припиняють своє існування. Наприклад, будинок має кімнати, і ці кімнати залежать від будинку.
Переваги
- Допомагає уникнути class explosion problem - успадкування може привести до величезної ієрархічної структури класів, яку важко зрозуміти та підтримувати. А також може збільшити ризик конфліктів імен методів та змінних.
- Гнучкість та мінімізація залежностей. При композиції відношення між двома класами вважаються слабко зв'язаними. Це означає, що зміни в класі компонента рідко впливають на клас композиту, і зміни в класі композиту ніколи не впливають на клас компонента. Це забезпечує кращу адаптивність до змін і дозволяє додаткам вводити нові вимоги, не впливаючи на існуючий код.
- Полегшує тестування - тестувати окремі компоненти легше, оскільки вони є менш залежними одне від одного.
Наслідування (is-a
) - Dog
— це Animal
, тобто собака є твариною.
class Animal:
def make_sound(self):
print("Some generic sound")
class Dog(Animal):
def make_sound(self):
print("Woof!")
dog = Dog()
dog.make_sound() # Woof!
Агрегація (has-a
- слабкий зв'язок) - Company
має Employees
, але якщо компанія закриється, співробітники можуть працювати в іншому місці.
class Employee:
def __init__(self, name):
self.name = name
def work(self):
print(f"{self.name} is working.")
class Company:
def __init__(self, name, employees):
self.name = name
self.employees = employees
def show_employees(self):
for emp in self.employees:
print(f"Employee: {emp.name}")
emp1 = Employee("Alice")
emp2 = Employee("Bob")
company = Company("TechCorp", [emp1, emp2])
company.show_employees()
del company
print(emp1.name) # Alice still exists
Композиція (has-a
- сильний зв'язок) - House
має Rooms
, але кімнати не можуть існувати без будинку.
class Room:
def __init__(self, name):
self.name = name
class House:
def __init__(self):
self.rooms = [Room("Kitchen"), Room("Bedroom")] # Rooms are created in the house
def show_rooms(self):
for room in self.rooms:
print(f"Room: {room.name}")
house = House()
house.show_rooms()
del house # We delete the house - the rooms also disappear
print(house.rooms) # Error because house no longer exists
Яка різниця між абстрактним класом і інтерфейсом?⚑
Абстрактний клас — це клас, який не можна інстанціювати і який може містити як абстрактні методи (без реалізації), так і методи з реалізацією. Абстрактні класи дозволяють створювати базові класи, що можуть містити спільну логіку для своїх підкласів. В Python абстрактні класи визначаються за допомогою модуля abc
і декоратора @abstractmethod
.
from abc import ABC, abstractmethod
class AbstractClass(ABC):
status: Enum
def common_method(self): # common method with implementation
print("This is a common method")
@abstractmethod
def abstract_method(self): # abstract method without implementation
pass
Інтерфейс, з іншого боку, визначає набір методів, які клас повинен реалізувати, але сам не містить жодної реалізації. В Python роль інтерфейсу можуть виконувати абстрактні класи, що містять лише абстрактні методи. Фактично, Python не має окремого поняття "інтерфейс", як, наприклад, у Java, але завдяки абстрактним класам можна досягти подібної поведінки.
from abc import ABC, abstractmethod
class InterfaceExample(ABC):
@abstractmethod
def method_one(self): # must be implemented by subclass
pass
@abstractmethod
def method_two(self): # must be implemented by subclass
pass
- Основна різниця полягає в тому, що абстрактний клас може містити як абстрактні методи, так і методи з реалізацією, тоді як інтерфейс (в термінах Python — абстрактний клас, що містить лише абстрактні методи) не має реалізації і служить лише для визначення контракту, який клас повинен виконувати.
- Абстрактні класи також можуть мати змінні та конструктори, тоді як інтерфейси зазвичай визначають тільки методи.