#14. Вступ до ООП
Вступ до ООП. Основні парадигми ООП. Класи та об’єкти.
Ми сприймаємо світ, який складається з об’єктів.
Рене Декарт
План
- ООП як методологія програмування
- Об’єкт та клас
- Принципи ООП
- Інкапсуляція
- Успадкування
- Поліморфізм
- Абстракція
- Практика і Домашнє завдання
- Література
Об’єктно-орієнтоване програмування (ООП, англ. Object-oriented programming, OOP) - парадигма програмування*, яка розглядає програму як множину «об’єктів», що взаємодіють між собою.
* Парадигма програмування - система ідей і понять, які визначають стиль написання комп’ютерних програм
Світ складається з безлічі об’єктів, які володіють певними властивостями, взаємодіють між собою та через це змінюються.
Об’єкт у програмуванні
Це певна сутність в віртуальному просторі, що володіє певним станом і поведінкою, має задані значення властивостей та операцій над ними.
Клас
шаблон для створення об’єктів
Клас - це спосіб опису сутності, який визначає її початковий стан та поведінку, а також правила взаємодії з цією сутністю.
Клас можна порівнювати з формою для випічки печива — форма одна, а печива можна випекти безліч. Печиво — це конкретні об’єкти, екземпляри класу печиво, яке може бути з різною начинкою.
Об’єкт - це екземпляр класу!
Основні принципи ООП:
- Все є об’єктами.
- Всі дії та розрахунки виконуються шляхом взаємодії (обміну даними) між об’єктами, під час якої один об’єкт потребує, щоб інший об’єкт виконав деяку дію. Об’єкти взаємодіють, надсилаючи й отримуючи повідомлення. Повідомлення — це запит на виконання дії, доповнений набором аргументів, які можуть знадобитися під час виконання дії.
- Кожен об’єкт має незалежну пам’ять.
- Кожен об’єкт є представником (екземпляром, примірником) класу, який виражає загальні властивості об’єктів.
- У класі задається поведінка (функціональність) об’єкта. Таким чином усі об’єкти, які є екземплярами одного класу, можуть виконувати одні й ті ж самі дії.
- Класи організовані у єдину деревоподібну структуру з загальним корінням, яка називається ієрархією успадкування. Пам’ять та поведінка, зв’язані з екземплярами деякого класу, автоматично доступні будь-якому класу, розташованому нижче в ієрархічному дереві.
Таким чином, програма є набором об’єктів, що мають стан та поведінку. Об’єкти взаємодіють використовуючи повідомлення. Будується ієрархія об’єктів: програма в цілому — це об’єкт, для виконання своїх функцій вона звертається до об’єктів що містяться у ньому, які своєю чергою виконують запит шляхом звернення до інших об’єктів програми.
Класи у Python
class Car:
...
car = Car() # створення обʼєкта
print(car) # <__main__.Car object at 0x104aadc90>
З точки зору програмування, клас можна розглядати як набір
- даних (полів, атрибутів, членів класу)
- функцій (методів) для роботи з ними
Поля (атрибути)
Змінні, опис яких створюється при створенні класу, називаються полями класу. Всі дані об’єкта зберігаються в його полях. Доступ до полів здійснюється за їх назвою.
class Pen:
color = 'red'
pen = Pen() # створення об'єкта
pen.color = 'black'
pen.price = 5.0
Методи
Функції, які працюють в контексті екземпляра класу. Таким чином, вони можуть змінювати стан екземпляра, звертаючись до атрибутів екземпляра, або виконувати будь-яку іншу корисну роботу.
class Pen:
color = 'red'
def write(self, text):
print(text)
p = Pen()
p.write('Hello!')
Параметр self
посилання на конкретний екземпляр класу
self представляє екземпляр класу. Використовуючи self, можливо отримати доступ до атрибутів та методів класу в Python. Він пов’язує атрибути з заданими аргументами. При цьому, саме ім’я self не є особливим, а лише угодою. Замість self можна використовувати іншу назву (наприклад, this), але так робити не варто.
При створенні об’єктів сам об’єкт передається в параметр self. Це зв’язує дані об’єкта з ним самим. Нижче наведено приклад того, як можна візуалізувати дані кожного об’єкта. Зверніть увагу, що self замінено на ім’я об’єкта.
Об’єкт передається в параметр self, щоб об’єкт міг зберігати свої власні дані. Думайте про процес створення об’єкта так: коли створюється об’єкт, він використовує клас як шаблон для своїх власних даних та методів. Якщо не передавати своє власне ім’я в параметр self, атрибути та методи в класі залишаться загальним шаблоном та не будуть посилатися (належати) на конкретний об’єкт. Отже, передача імені об’єкта в параметр self означає, що якщо з одного класу створено 100 об’єктів, вони всі зможуть відстежувати власні дані та методи.
Причина, чому потрібно використовувати self, полягає в тому, що у Python методи працюють таким чином, що екземпляр, до якого належить метод, передається автоматично, але не отримується автоматично: перший параметр методів є екземпляром, на якому викликається метод.
Трішки коду
# Використовуємо ключове слово class
class Car:
# Опишемо клас Car, у якому будуть два атрибути - колір і максимальна швидкість.
color = 'червоний'
top_speed = 250
# І декілька методів.
# Повернути рядок з максимальною швидкістю та кольором
def get_color_and_top_speed(self):
return f'максимальна швидкість: {self.top_speed};колір: {self.color}'
# Повернути булеве значення, яке відповідає на питання, чи може машина їхати з потрібною швидкістю
def is_car_can_go_with_needed_speed(self, speed):
return speed < self.top_speed
# Призначити максимальну швидкість
def set_top_speed(self, speed):
self.top_speed = speed
# Як створити об'єкт? для цього просто потрібно "викликати" клас
car_1 = Car()
print(car_1.get_color_and_top_speed()) # максимальна швидкість: 250;колір: червоний
print(car_1.is_car_can_go_with_needed_speed(200)) # True
# Доступ до атрибутів можна отримати безпосередньо
print(car_1.top_speed) # 250
car_1.set_top_speed(150) # Призначаємо нову максимальну швидкість для цього об'єкта
print(car_1.is_car_can_go_with_needed_speed(200)) # False
car_2 = Car()
print(car_2.get_color_and_top_speed()) # максимальна швидкість: 250;колір: червоний
Парадигми ООП
ООП базується на трьох основних і одній вторинній парадигмах.
- Інкапсуляція
- Успадкування
- Поліморфізм
- Абстракція
Інкапсуляція
(англ. encapsulation, від лат. in capsula)
Приховування внутрішньої реалізації роботи класів від об’єктів, що їх використовують шляхом розміщення в одному компоненті даних та методів, які з ними працюють.
Наприклад, доступ до прихованої змінної може надаватися не безпосередньо, а за допомогою методів читання (getter) та зміни (setter) її значення.
def to_digital_time(time):
return time
def to_analog_time(time):
return time
class Watch:
time = None
def set_time(self, time):
if self.is_valid(time):
self.time = to_digital_time(time)
def get_time(self):
return to_analog_time(self.time)
def is_valid(self, time):
return True
Інкапсуляція дозволяє приховувати деталі реалізації об’єкту від користувача, тим самим забезпечуючи високий рівень абстракції та захисту даних.
Ще один приклад, це кавомашина - є досить складний з інженерної точки зору механізм, але для користувача є набір кнопок, натиснувши на яку можна отримати конкретний результат.
Під інкапсуляцією розуміють як і те, що об’єкт вміщує не тільки дані, але і правила їх обробки, оформлені в вигляді виконуваних фрагментів (методів). А також те, що доступ до стану об’єкта напряму може бути заборонений, і ззовні з ним можна взаємодіяти виключно через заданий інтерфейс (відкриті поля та методи), що дозволяє знизити зв’язність. Таким чином контролюються звернення до полів класів та їхня правильна ініціалізація, усуваються можливі помилки пов’язані з неправильним викликом методу. Оскільки користувачі працюють лише через відкриті елементи класів, то розробники класу можуть як завгодно змінювати всі закриті елементи і, навіть, перейменовувати та видаляти їх, не турбуючись, що десь хтось їх використовує у своїх програмах.
У Python інкапсуляція дуже умовна (завжди можна отримати доступ будь-куди, було б бажання). Як сказав творець мови, Гвідо Ван Россум, всі ми дорослі люди, навіщо будемо когось обмежувати.
Насамперед код пишеться для людей, тому й поділ існує на рівні розуміння людей.
Існує три види стану атрибутів та властивостей, і для їхнього поділу використовується спеціальний синтаксис.
Атрибути та методи, чиї назви починаються з букв, називаються public. І вони доступні скрізь. В об’єкті, у класі, у наслідуванні.
Атрибути та методи чиї назви починаються із символу _. Вони називаються protected, і мається на увазі, що ми їх використовуватимемо виключно в класі і в успадкування, але не будемо використовувати в об’єктах.
Атрибути та методи які починаються з __. Вони називаються private і мається на увазі, що ми їх використовуватимемо виключно всередині самого класу.
class Car:
color = 'red'
_top_speed = 250
__max_carrying = 1000
def find_color_and_top_speed(self):
return 'this car top speed is {} and color is {}'.format(self._top_speed, self.color)
def is_can_go_with_needed_speed(self, speed):
return speed < self._top_speed
def is_can_get_weight(self, weight):
return self.__max_carrying > weight
def change_max_carrying(self, new_carrying):
self.__max_carrying = new_carrying
def __private_method(self):
print('this is private method')
def _this_is_protected_method(self):
print('this is protected method')
def run_hidden_and_protected_methods(self):
self.__private_method()
self._this_is_protected_method()
car = Car()
car.color # все нормально
car._top_speed # спрацює, але ми самі описали цю властивість так що б повідомити, що не треба так його використовувати
car.__max_carrying # не спрацює (буде помилка, що цей атрибут не знайдено)
car._Car__max_carrying # Спрацює і це якраз опис того, що дістатися можна куди завгодно. Але сам синтаксис нам каже, що ми щось робимо
car.__max_carrying = 800 # не спрацює
car.change_max_carrying(800) # спрацює
сar.__hidden_method() # не спрацює (буде помилка, що цього методу не існує)
car._this_is_protected_method() # спрацює, але знову ж таки не треба цього робити
car.run_hidden_and_protected_methods() # спрацює та викличе захищений та приватний методи
Успадкування
(англ. inheritance)
Опис нового класу на основі вже існуючого з частковим або повним запозиченням функціональності.
Клас, від якого робиться успадкування, називається базовим чи батьківським.
Новий клас – нащадком, підкласом чи похідним класом.
Успадкування дає класу можливість використовувати програмний код іншого (базового) класу, доповнюючи його своїми власними деталями реалізації.
class Animal:
name = ''
def get_name(self):
return self.name
class Dog(Animal):
breed = ''
def get_breed(self):
return self.breed
dog = Dog()
dog.name = 'Bob'
dog.breed = 'doberman'
print(dog.get_name()) # Bob
print(dog.get_breed()) # doberman
Таким чином, успадкування дозволяє повторно використовувати код і будувати їєрархію класів.
Метод super
Практично завжди, коли нам потрібно у похідному класі виконати таку ж дію як і в батьківському, нам необхідно ґрунтуватися на даних із батьківського.
Але як викликати код із нащадку? Допоможе метод super
Припустимо, у нас є клас, який займається тим, що просто повертає нам ціну продукту. І ще два класи, які вважають знижку для ціни в 20%. І друге ще 20% вже від зменшеної ціни.
class PriceCounter:
price = 100
def calculate_price(self):
print('In original price')
return self.price
class DiscountCounter(PriceCounter):
def calculate_price(self):
print('In discount calculate')
return super().calculate_price() * 0.8
class SuperDiscountCounter(DiscountCounter):
def calculate_price(self):
print('In super discount calculate')
return super().calculate_price() * 0.8
price_counter = PriceCounter()
discount_counter = DiscountCounter()
super_discount_counter = SuperDiscountCounter()
print(price_counter.calculate_price())
""" In original price 100 """
print(discount_counter.calculate_price())
""" In discount calculate In original price 80.0 """
print(super_discount_counter.calculate_price())
""" In super discount calculate In discount calculate In original price 64.0 """
Функція super, приймає в якості аргументів клас і self. За замовчування, ці аргументи прокидаються автоматично.
class A:
def hi(self):
print("A")
class B(A):
def hi(self):
print("B")
class C(B):
def hi(self):
super(B, self).hi()
c = C()
c.hi() # A
Застарілі синтаксиси
Для Python існують кілька різних версій, включаючи 2.х та 3.х
Версії 2.х вважаються застарілими, але все ж таки іноді можна зустріти код на другій версії.
class A(): # Варіант з python2, працюватиме
pass
class B(object):
"""
Варіант який теж працюватиме і насправді показує нам суть будь-якого класу та об'єкта в Python, взагалі все успадковано від об'єкту """
class C: # Традиційний спосіб оголошення класу в Python3
pass
Методи type, isinstance и issubclass
Будь-який об’єкт завжди має тип даних. І за фактом цим типом даних завжди є клас.
У Python все є об’єктом!
Це означає, що все в мові - змінні, функції, класи і т. д. - можуть бути розглянуті як об’єкти, з якими можна взаємодіяти та виконувати операції, такі як передача в якості аргументів до інших функцій, створення нових об’єктів і т.д. (так, str, int, list і т. і навіть функції це теж класи)
Щоб дізнатися тип даних будь-якого об’єкта необхідно викликати метод type
class A:
pass
a = A()
num = 10
text = 'test_str'
collection = [1, 2, 3]
def some_func():
pass
print(type(a)) # <class '__main__.A'>
print(type(num)) # <class 'int'>
print(type(text)) # <class 'str'>
print(type(collection)) # <class 'list'>
print(type(some_func)) # <class 'function'>
Для того, щоб дізнатися чи, є об’єкт підкласом існує спеціальна функція istanstance, яка приймає на вхід об’єкт і клас, або кортеж з класів, а повертає bool.
І є така сама функція, яка приймає не об’єкт, а сам клас - issubclass.
class A:
pass
class B(A):
pass
class C(B):
pass
a = A()
b = B()
c = C()
print(isinstance(a, A)) # True
print(isinstance(a, B)) # False
print(isinstance(b, A)) # True
print(isinstance(c, (A, C))) # True
print(issubclass(type(a), A)) # True
print(issubclass(B, A)) # True
print(issubclass(A, C)) # False
print(issubclass(type(a), (B, C))) # False
Поліморфізм
(англ. polymorphism, з грец. πολύς «багато» + μορφή «форма»)
Різна поведінка одного й того методу в різних класах. Метод, успадкований від базового класу, можна змінювати у похідних класах.
По суті, це можливість використовувати одні й самі методи чи інтерфейси до різних структур, наприклад звичайний знак +, адже ми можемо скласти числа, а можемо і рядки, і отримаємо різний результат, але застосуємо один і той самий метод.
class Animal:
def say(self):
...
class Dog(Animal):
def say(self):
return f'woof'
class Cat(Animal):
def say(self):
return f'meow'
dog = Dog()
cat = Cat()
print(dog.say()) # woof
print(cat.say()) # meow
animals = (dog, cat)
for animal in animals:
print(animal.say())
Абстракція
(англ. abstraction)
Сукупність усіх важливих характеристик об’єкта, що відрізняють його від інших об’єктів.
Абстрагування - це виділення набору важливих характеристик об’єкта, виключаючи з розгляду неважливі. Основна ідея полягає в тому, щоб представити об’єкт мінімальним набором полів та методів, що при цьому є достатньо точним для вирішення поставленої задачі.
class Pen:
color = 'black'
icon = 'pen.jpg'
Абстрактний метод - метод класу, реалізація для якого відсутня. Клас, що містить абстрактні методи, також називають абстрактними.
Інтерфейс - це абстрактний клас, у якого жоден метод не реалізований, всі вони публічні і немає змінних класу.
Інтерфейс (від англ. interface — поверхня розділу, перегородка) — сукупність засобів, методів і правил взаємодії (керування, контролю, тощо) обʼєктів між собою.
class Animal:
def say(self):
raise NotImplementedError # повинні реалізувати метод saу
Замість висновків
Плюси ООП
- Візуально код стає простішим, і його легше читати. Коли все розбито на об’єкти і вони мають зрозумілий набір правил, можна відразу зрозуміти, що відповідає кожен об’єкт і що він складається.
- Менше однакового коду.
- Складні програми пишуться легше. Кожну велику програму можна розкласти на кілька блоків, зробити їм мінімальне наповнення, а потім детально описати кожен блок.
- Збільшується швидкість написання. На старті можна швидко створити потрібні компоненти усередині програми, щоб отримати мінімально працюючий прототип.
Мінус ООП
- Важко зрозуміти та почати працювати. Підхід ООП набагато складніший за звичайне процедурне програмування — потрібно знати теорію, перш ніж буде написано хоч один рядок коду.
- Потребує більше пам’яті.
Об’єкти в ООП складаються з даних, інтерфейсів, методів та багато іншого, а це займає набагато більше пам’яті, ніж проста змінна. - Іноді продуктивність коду буде нижчою. Через особливості підходу частина речей може бути реалізована складніше, ніж могла бути. Тому буває таке, що ООП-програма працює повільніше, ніж процедурна (хоча із сучасними потужностями процесорів це мало кого хвилює).
Практика і домашнє завдання:
1. Описуємо телефон:
Клас телефону.
У нього мають бути:
- Поле для опису номера
- Метод, щоб задати номер телефону
- Захищене поле для лічильника вхідних дзвінків
- Метод, який поверне нам кількість прийнятих дзвінків
- Метод прийняти дзвінок, який додає до лічильника одиницю
Створіть три різні об’єкти телефону. Поміняйте всім початковий номер. Прийміть по кілька дзвінків на кожному (різна кількість)
Напишіть функцію, яка приймає список з об’єктів телефонів, а повертає загальну кількість прийнятих дзвінків з усіх телефонів.
* Зберігати інформацію про прийняті дзвінки у файл (txt або краще csv)
2. Опишіть клас для фігури шахів.
Фігура повинна містити такі атрибути:
- Колір (білий або чорний)
- Місце на дошці (тут є варіанти, або два окремих поля, для опису координат або одне, але, наприклад, кортеж з двох чисел)
І такі методи як: - Змінити колір (нічого не приймає, тільки змінює колір на протилежний)
- Змінити місце на дошці (приймає або дві змінні або один кортеж з двох елементів), не забудьте перевірити, що ми не намагаємося поставити фігуру за межі дошки (обидва значення від 0 до 7)
- Абстрактний метод перевірки потенційного ходу (деталі нижче)
На даному етапі фігури можуть стояти на одній і ті ж клітині, поки нам це не важливо
Опишіть класи, для пішака, коня, офіцера, тури, ферзя та короля. Все що в них потрібно додати - це один метод для перевірки, чи можливо за один хід поміняти місце фігури на дошці (всі ходять по-різному, пішаки мають ще й відмінність від кольору). Метод приймає знову ж таки або дві цифри, або один кортеж. І знову ж таки перевіряємо чи не виходить значення за межі дошки (оскільки нам потрібен цей функціонал двічі, бажано робити його як окремий захищений метод у батьківському класі).
І функцію, яка приймає список фігур та потенційну нову клітинку, а повертає список із фігур. Але тільки тих, які можуть за один хід дістатися цієї клітини.
* Скрізь описати типізації (у функціях, атрибутах та методах)