У цій статті ми зануримося у світ об'єктно-орієнтованого програмування (OOP), створюючи практичну систему управління книжковим магазином. Перед тим, як почати, переконайтесь, що ви знайомі з основними концепціями програмування, такими як змінні, функції, оператори умови (if), та цикли (for). Якщо ви маєте досвід роботи з цими концепціями в будь-якій мові програмування, ви готові слідувати далі!
Повний код цього проекту доступний у моєму репозиторії на GitHub.
Вступ до OOP у Python
Тут ми визначили змінні для book1
, book1_price
, book1_quantity
і book1_total_price
. Щоб полегшити асоціацію цих змінних для людини, ми додали префікс book1
, щоб вказати, що вони стосуються однієї і тієї ж книги.
book1 = "Learning Python"
book1_price = 10
book1_quantity = 5
book1_total_price = book1_price * book1_quantity
Розглянемо типи даних цих змінних:
print(type(book1)) # Output:
print(type(book1_price)) # Output:
print(type(book1_quantity)) # Output:
print(type(book1_total_price)) # Output:
Як бачите, для Python ці змінні є чотирма окремими змінними зі своїми власними типами даних. book1
є рядком, а book1_price
, book1_quantity
і book1_total_price
— цілими числами.
Зверніть увагу, що для кожного типу результат містить ключове слово class
. Це означає, що ці типи даних фактично є екземплярами своїх відповідних класів.
Ключова ідея: Все є об'єктом
У Python кожен тип даних є об'єктом, створеним з попередньо визначеного класу. Наприклад:
- Змінна
book1
є об'єктом, створеним із класу рядка. - Точно так само
book1_price
,book1_quantity
іbook1_total_price
є об'єктами, створеними з класу цілих чисел.
Це підкреслює одну з найпотужніших особливостей Python: всі типи даних є об'єктами, створеними на основі класів. Клас фактично визначає шаблон для створення об'єктів, а самі об'єкти є екземплярами цих класів.
Визначення класів у Python
Чи не було б чудово, якби ми могли визначити власні типи даних у Python? Наприклад, ми могли б визначити клас для книг і включити такі атрибути, як назва, ціна та кількість.
Створення класу
Давайте визначимо клас з іменем Book
. Спочатку ми не додаватимемо жодної функціональності, тому використаємо оператор pass
, щоб уникнути помилок:
class Book:
pass
Створення екземплярів
Тепер, коли ми визначили клас, можемо створювати екземпляри або об'єкти цього класу. Кожен екземпляр буде представляти окрему книгу.
class Book:
pass
book1 = Book()
Присвоєння атрибутів класу
Атрибути дозволяють зберігати конкретні деталі про екземпляр. Ми можемо створювати їх за допомогою оператора крапка (.
) після імені екземпляра. Давайте визначимо кілька атрибутів, таких як title
, price
і quantity
:
class Book:
pass
book1 = Book()
book1.title = "Learning Python"
book1.price = 10
book1.quantity = 5
Тепер ці атрибути пов'язані з одним екземпляром класу. Це створює чітке відношення між атрибутами (title
, price
, quantity
) та об'єктом (book1
). Давайте перевіримо це, розглянувши типи як екземпляра, так і його атрибутів:
print(type(book1)) # Output:
print(type(book1.title)) # Output:
print(type(book1.price)) # Output:
print(type(book1.quantity)) # Output:
Тип даних book1
відрізняється від окремих змінних, які ми створювали раніше.
Тип даних `вказує, що
book1є екземпляром (instance) власного класу
Book`, що означає, що ми фактично створили власний тип даних.
Методи в класах Python
У Python методи — це функції, визначені всередині класу, які призначені для роботи з екземплярами цього класу. Їх можна викликати за допомогою синтаксису instance.method_name()
. Це з'єднання дозволяє методам взаємодіяти з даними, що містяться в екземплярі, і змінювати їх.
Параметр self
Коли ви визначаєте метод всередині класу, Python вимагає, щоб він приймав хоча б один параметр. Цей параметр, зазвичай званий self
, представляє екземпляр класу, який викликає метод, і дозволяє доступ до атрибутів екземпляра та інших методів.
Хоча технічно ви могли б використати будь-яке ім'я для цього параметра, дотримання конвенції self
забезпечує узгодженість і зручність для читання в проектах на Python.
Наприклад, розглянемо наступний метод:
class Book:
def calculate_total_price(self):
pass
Тут параметр self
дозволяє методу доступати до атрибутів і методів екземпляра, коли він буде реалізований. Якщо ви видалите self
і спробуєте викликати метод:
book1 = Book()
book1.calculate_total_price()
Python виведе помилку:
TypeError: Book.calculate_total_price() takes 0 positional arguments but 1 was given
Ця помилка виникає тому, що Python автоматично передає екземпляр (book1
) як перший аргумент методу. Без self
в оголошенні методу немає параметра, який міг би отримати цей аргумент.
Визначення та використання методів
Давайте створимо метод для обчислення загальної ціни книг на основі ціни та кількості. Назвемо його calculate_total_price
і визначимо його для прийому двох параметрів — x
та y
, що представляють ціну та кількість відповідно. Метод буде повертати їх добуток:
class Book:
def calculate_total_price(self, x, y):
return x * y
Тепер ми можемо створювати екземпляри класу Book
і використовувати цей метод:
book1 = Book()
book1.title = "Learning Python"
book1.price = 10
book1.quantity = 5
print(book1.calculate_total_price(book1.price, book1.quantity)) # Output: 50
book2 = Book()
book2.title = "Two Scoops of Django"
book2.price = 30
book2.quantity = 10
print(book2.calculate_total_price(book2.price, book2.quantity)) # Output: 300
Метод init
Вищезгаданий підхід працює, але є неефективним, оскільки потрібно вручну визначати атрибути для кожного екземпляра. Ми можемо виправити це, використовуючи метод __init__
, також відомий як конструктор.
Метод __init__
є спеціальним методом в Python, який виконується автоматично щоразу, коли створюється екземпляр класу. Це дозволяє ініціалізувати атрибути безпосередньо під час створення екземпляра.
Ось як ми визначаємо метод __init__
:
class Book:
def __init__(self):
print(f"A new book has been added.")
book1 = Book()
book2 = Book()
Коли ми виконуємо код, повідомлення "A new book has been added.
" буде надруковано двічі — один раз для кожного екземпляра (book1
та book2
). Це відбувається тому, що Python автоматично викликає метод __init__
кожного разу, коли ми створюємо новий екземпляр класу.
Покращення ініціалізації
Давайте покращимо метод __init__
для нашої системи управління книжковим магазином.
Замість того, щоб вручну встановлювати атрибути, як-от title
, price
та quantity
, для кожного екземпляра після його створення, ми можемо передавати ці атрибути як параметри методу __init__
.
Параметр self
в методі __init__
дозволяє нам динамічно призначати ці атрибути екземплярам, що гарантує їх правильну ініціалізацію під час створення об'єкта.
Ось як ми можемо цього досягти:
class Book:
def __init__(self, title, price, quantity):
self.title = title
self.price = price
self.quantity = quantity
def calculate_total_price(self, x, y):
return x * y
book1 = Book("Learning Python", 10, 5)
book2 = Book("Two Scoops of Django", 30, 10)
print(book1.title) # Output: Learning Python
print(book1.price) # Output: 10
print(book1.quantity) # Output: 5
print(book2.title) # Output: Two Scoops of Django
print(book2.price) # Output: 30
print(book2.quantity) # Output: 10
Метод __init__
використовується для визначення атрибутів, які об'єкт має мати, коли він створюється. Переміщуючи title
, price
та quantity
в метод __init__
, ми забезпечуємо, що кожен об'єкт Book
ініціалізується з цими атрибутами.
Уникнення надмірних параметрів у методах
У Python кожен метод в класі автоматично отримує екземпляр, на якому він викликається, як перший аргумент, зазвичай званий self
. Це означає, що метод calculate_total_price
вже має доступ до атрибутів price
та quantity
через self
.
Ось оновлена реалізація класу Book
:
class Book:
def __init__(self, title, price, quantity):
self.title = title
self.price = price
self.quantity = quantity
def calculate_total_price(self):
return self.price * self.quantity
book1 = Book("Learning Python", 10, 5)
book2 = Book("Two Scoops of Django", 30, 10)
print(book1.calculate_total_price()) # Output: 50
print(book2.calculate_total_price()) # Output: 300
Звертаючись до self.price
і self.quantity
, немає потреби явно передавати значення x
та y
при виклику методу. Це демонструє важливу особливість методів екземплярів і роль конструкторів в класах.
Використання значень за замовчуванням у методах
Методи можуть включати як обов'язкові, так і необов'язкові параметри, використовуючи значення за замовчуванням. Наприклад, якщо кількість книги спочатку невідома, ми можемо встановити значення за замовчуванням для параметра quantity
, рівне 0
:
class Book:
def __init__(self, title, price, quantity=0):
self.title = title
self.price = price
self.quantity = quantity
def calculate_total_price(self):
return self.price * self.quantity
book1 = Book("Learning Python", 10)
book2 = Book("Two Scoops of Django", 30, 10)
print(book1.calculate_total_price()) # Output: 0
print(book2.calculate_total_price()) # Output: 300
Значення за замовчуванням, як-от quantity=0
, роблять ваш код більш гнучким. Вони дозволяють обробляти неповні дані без проблем, гарантуючи, що ваша програма не зламається, якщо деякі атрибути будуть відсутні під час створення об'єкта.
Додавання атрибутів до екземплярів динамічно
Класи Python дозволяють динамічно призначати атрибути конкретним екземплярам без необхідності визначати їх у конструкторі. Наприклад, щоб відстежувати, чи є книга частиною спеціальної колекції, ви можете безпосередньо додати цей атрибут до екземпляра:
book1.special_edition = True
Обробка помилок в ООП
Розглянемо, що станеться, якщо передати некоректний тип даних у клас. Наприклад, якщо випадково передати рядок замість числа:
book = Book("The Great Gatsby", "10", 3)
print(book.calculate_total_price())
Замість помилки, ви можете спостерігати, як Python поводиться неочікувано.
У цьому випадку Python трактує множення як повторення рядка, що призводить до такого виводу:
101010
Це підкреслює важливість перевірки типів даних для значень, які ми передаємо в наші класи.
Додавання підказок типів (Type Hints)
Підказки типів (Type Hints) дозволяють нам вказати очікувані типи для параметрів, що покращує зрозумілість коду та зменшує ризик помилок під час виконання. Наприклад, ми можемо оголосити, що title
повинен бути рядком (str), price
— числом з плаваючою точкою (float), а quantity
може мати тип за замовчуванням — ціле число (int):
class Book:
def __init__(self, title: str, price: float, quantity: int = 0):
self.title = title
self.price = price
self.quantity = quantity
Забезпечення валідації даних
Хоча підказки типів (Type Hints) корисні, вони не забезпечують перевірку коректності під час виконання. Якщо буде передано несумісний тип даних, інструменти Python, такі як лінтери або IDE, можуть виявити проблему, але програма все одно запуститься, якщо не буде додаткової перевірки.
Щоб забезпечити правильність типів, ви можете додати ручні перевірки в конструктор класу:
class Book:
def __init__(self, title: str, price: float, quantity: int = 0):
assert price >= 0, f"Price {price} is not greater or equal to zero"
assert quantity >= 0, f"Quantity {quantity} is not greater or equal to zero"
self.title = title
self.price = price
self.quantity = quantity
З такими перевірками, якщо price
або quantity
буде від'ємним, виникне помилка AssertionError
з чітким повідомленням. Це допоможе виявити помилки на ранніх етапах виконання програми:
book = Book("The Great Gatsby", 10, -1)
print(book.calculate_total_price())
Давайте подивимось, як працює вдосконалений клас:
AssertionError: Quantity -1 is not greater or equal to zero
Розуміння атрибутів
У Python атрибути, які асоціюються з об'єктами, поділяються на два типи:
- Атрибути екземпляра (Instance Attributes): Вони є унікальними для кожного екземпляра класу. Кожен об'єкт має свою копію, що дозволяє налаштовувати атрибути індивідуально для кожного екземпляра.
- Атрибути класу (Class Attributes): Вони спільні для всіх екземплярів класу. Вони визначені на рівні класу і залишаються однаковими для всіх екземплярів, якщо їх не змінити вручну.
У наведеному прикладі discount_rate
є атрибутом класу. Він доступний для всіх екземплярів класу Book
. При зверненні через екземпляр Python спочатку перевіряє атрибути екземпляра, і якщо їх там не знаходить, шукає серед атрибутів класу.
class Book:
discount_rate = 0.2
def __init__(self, title: str, price: float, quantity: int = 0):
assert price >= 0, f"Price {price} is not greater or equal to zero"
assert quantity >= 0, f"Quantity {quantity} is not greater or equal to zero"
self.title = title
self.price = price
self.quantity = quantity
def calculate_total_price(self):
return self.price * self.quantity
book1 = Book("Learning Python", 10, 5)
book2 = Book("Two Scoops of Django", 30, 10)
Звернення до атрибута класу безпосередньо через клас:
print(Book.discount_rate) # Output: 0.2
Звернення до атрибута класу через екземпляр:
print(book1.discount_rate) # Output: 0.2
print(book2.discount_rate) # Output: 0.2
Перегляд усіх атрибутів класу:
print(Book.__dict__) # Output: All attributes defined at the class level, including discount_rate
Перегляд атрибутів екземплярів для конкретних об'єктів:
print(book1.__dict__) # Output: {'title': 'Learning Python', 'price': 10, 'quantity': 5}
print(book2.__dict__) # Output: {'title': 'Two Scoops of Django', 'price': 30, 'quantity': 10}
Використання атрибутів класу в методах
Ми хочемо створити метод apply_discount
, який змінює ціну книги, застосовуючи ставку знижки.
discount_rate
визначено як атрибут класу, оскільки він спільний для всіх екземплярів.
class Book:
discount_rate = 0.2
def __init__(self, title: str, price: float, quantity: int = 0):
self.title = title
self.price = price
self.quantity = quantity
def apply_discount(self):
self.price = self.price * (1 - Book.discount_rate)
Тут discount_rate
доступний через ім'я класу Book.discount_rate
. Це гарантує, що всі екземпляри використовують одну й ту саму ставку знижки. Наприклад:
book1 = Book("Learning Python", 10, 5)
book1.apply_discount()
print(book1.price) # Output: 8.0
Метод apply_discount
працює як очікується, застосовуючи знижку 20%.
Перезаписування атрибутів класу
Якщо ми хочемо використати іншу ставку знижки для конкретної книги, наприклад, застосувати знижку 10% до Two Scoops of Django
, залишаючи знижку 20% для Learning Python
, зміна атрибута класу discount_rate
не підходить, оскільки це вплине на всі книги. Натомість, ми можемо призначити індивідуальний атрибут екземпляра:
book2 = Book("Two Scoops of Django", 30, 10)
book2.discount_rate = 0.1
book2.apply_discount()
print(book2.price) # Output: 24.0
Однак, у початковій реалізації методу apply_discount
, метод використовує Book.discount_rate
, що завжди звертається до атрибута класу. Як результат, атрибут екземпляра discount_rate
буде проігноровано. Щоб це виправити, ми повинні використовувати self.discount_rate
:
class Book:
def apply_discount(self):
self.price = self.price * (1 - self.discount_rate)
Тепер, якщо ми не перезаписуємо discount_rate
для book1
, він за замовчуванням використовує атрибут класу. З іншого боку, для book2
атрибут екземпляра discount_rate
(який встановлений на 0.1) перезаписує атрибут класу. Коли ми виконуємо код:
book1 = Book("Learning Python", 10, 5)
book2 = Book("Two Scoops of Django", 30, 10)
book1.apply_discount()
print(book1.price) # Output: 8.0
book2.discount_rate = 0.1
book2.apply_discount()
print(book2.price) # Output: 27.0
Для book1
буде застосована знижка 20%, а для book2
— знижка 10%.
Відстеження та управління всіма екземплярами класу
Зараз наша програма не має централізованого механізму для відстеження всіх екземплярів, які ми створили. Коли ваш інвентар збільшиться, підтримка списку всіх екземплярів може бути корисною для різних операцій.
Давайте створимо більше екземплярів класу Book
:
book1 = Book("Learning Python", 10, 5)
book2 = Book("Two Scoops of Django", 30, 10)
book3 = Book("Clean Code", 25, 7)
book4 = Book("Design Patterns in Python", 40, 3)
book5 = Book("Fluent Python", 60, 2)
Ми можемо ввести атрибут класу для зберігання списку всіх створених екземплярів. Спочатку визначимо атрибут all
як порожній список на рівні класу:
class Book:
all = []
Далі, оновимо метод __init__
, щоб додавати кожен новий екземпляр до списку all
:
class Book:
...
all = []
def __init__(self, title: str, price: float, quantity: int = 0):
...
Book.all.append(self)
Тепер, коли створюється новий екземпляр класу Book
, він автоматично додається до списку Book.all
. Для перевірки можемо вивести список Book.all
:
print(Book.all)
Коли ви виконаєте код, ви побачите, що список Book.all
містить всі екземпляри:
[<__main__.Book object at 0x00000279F66CA720>, <__main__.Book object at 0x00000279F66C81A0>, <__main__.Book object at 0x00000279F66CA750>, <__main__.Book object at 0x00000279F66CA7B0>, <__main__.Book object at 0x00000279F66CA8A0>]
Покращення представлення екземплярів
Хоча вищенаведений вивід технічно правильний, він не надає багато корисної інформації. Щоб зробити представлення кожного екземпляра більш зручним для читання, ми можемо визначити метод __repr__
в класі Book
.
Цей метод контролює, як екземпляри класу представляються у вигляді рядків:
def __repr__(self):
return f"Book('{self.title}', {self.price}, {self.quantity})"
Після додавання цього методу, виведення Book.all
дасть такий результат:
[Book('Learning Python', 10, 5), Book('Two Scoops of Django', 30, 10), Book('Clean Code', 25, 7), Book('Design Patterns in Python', 40, 3), Book('Fluent Python', 60, 2)]
Практичне використання атрибута all
Атрибут all
дозволяє легко маніпулювати всіма екземплярами. Наприклад, щоб вивести назви всіх книг:
for instance in Book.all:
print(instance.title)
Результат буде таким:
Learning Python
Two Scoops of Django
Clean Code
Design Patterns in Python
Fluent Python
Робота з даними за допомогою CSV файлів
До цього часу ми зберігали дані безпосередньо у файлі main.py
, створюючи кілька елементів вручну. Однак, коли програма зростатиме, зберігання коду і даних в одному файлі ускладнить додавання нових функцій.
Ми могли б використати базу даних для керування даними, але для цілей цього уроку ми залишимо все простіше і використаємо CSV файл. CSV (Comma-Separated Values) — це формат, у якому дані зберігаються у табличній структурі, де кожен рядок представляє запис, а кожен стовпчик — поле.
Створення CSV файлу
Ми створимо файл з ім'ям books.csv
, щоб зберігати наші дані у такому форматі:
Title,Price,Quantity
"Learning Python",10,5
"Two Scoops of Django",30,10
"Clean Code",25,7
"Design Patterns in Python",40,3
"Fluent Python",60,2
Перший рядок містить заголовки стовпців: Title
, Price
та Quantity
, а наступні рядки містять значення даних, розділені комами.
Читання даних з CSV
Для роботи з CSV файлами в Python ми будемо використовувати вбудовану бібліотеку csv
. Почнемо з її імпорту:
import csv
Створення методу класу
Далі ми створимо метод для завантаження даних з файлу books.csv
. Цей метод спеціально призначений для створення екземплярів класу, тому його не можна викликати на екземплярі класу. Замість цього, він повинен бути доступний на рівні класу, ось так:
Book.instantiate_from_csv()
Щоб цього досягти, потрібно перетворити метод на метод класу. Додавши декоратор @classmethod
перед нашим методом, ми вказуємо, що цей метод належить самому класу, а не конкретному екземпляру класу. Метод автоматично отримує клас як перший параметр, який за умовою називається cls
замість self
.
Ось приклад реалізації:
class Book:
@classmethod
def instantiate_from_csv(cls):
pass
Book.instantiate_from_csv()
Читання CSV файлу та створення об'єктів
Ось приклад методу класу, який читає CSV файл і створює об'єкти на основі його вмісту. У цьому прикладі Python файл (main.py
) і CSV файл (books.csv
) знаходяться в одній директорії.
@classmethod
def instantiate_from_csv(cls):
with open("books.csv", "r") as f:
reader = csv.DictReader(f)
books = list(reader)
for book in books:
print(book)
У цьому коді ми відкриваємо CSV файл і читаємо його вміст як словник. Потім перетворюємо цей словник на список, оскільки списки дозволяють ітеруватися через елементи і виводити їх для перегляду структури.
При виконанні коду ви побачите, що кожен рядок з CSV файлу буде виведений як словник:
{'Title': 'Learning Python', 'Price': '10', 'Quantity': '5'}
{'Title': 'Two Scoops of Django', 'Price': '30', 'Quantity': '10'}
{'Title': 'Clean Code', 'Price': '25', 'Quantity': '7'}
{'Title': 'Design Patterns in Python', 'Price': '40', 'Quantity': '3'}
{'Title': 'Fluent Python', 'Price': '60', 'Quantity': '2'}
Замість того, щоб виводити кожну книгу, ми можемо створювати об'єкти, передаючи значення зі словника як аргументи.
Наприклад:
for book in books:
Book(
title=book.get("Title"),
price=float(book.get("Price")),
quantity=int(book.get("Quantity")),
)
Необхідно перетворити Price
і Quantity
на float
і int
, оскільки ці значення зчитуються з CSV файлу як рядки. Без цього перетворення виникне помилка при спробі використати ці значення як числа.
Статичні методи
Статичні методи працюють як звичайні окремі функції, що належать класу, але приймають тільки параметри, явно визначені в їх сигнатурі, що робить їх ідеальними для функціональності, спільної для всього класу, яка не потребує доступу до даних на рівні екземпляра або класу.
Для визначення статичного методу використовується декоратор @staticmethod
, що гарантує, що метод не отримає посилання на екземпляр (self
) або клас (cls
) як перший аргумент.
@staticmethod
def is_integer(num):
if isinstance(num, float):
return num.is_integer()
elif isinstance(num, int):
return True
else:
return False
У цьому прикладі статичний метод is_integer
перевіряє, чи є задане число цілим або числом типу float, яке виглядає як ціле. Ось як він працює з різними вхідними даними:
print(Book.is_integer(7)) # True
print(Book.is_integer(7.0)) # True
print(Book.is_integer(7.5)) # False
Метод повертає True
для дійсних цілих чисел або чисел типу float, які виглядають як цілі, і False
в іншому випадку.
Наслідування в Python
Ми створимо два екземпляри класу Book
та додамо новий атрибут damaged_copies
для відстеження кількості пошкоджених друкованих книжок. Це реалістичне доповнення, яке допоможе нам слідкувати за запасами:
book1 = Book("Introduction to Algorithms", 45.0, 10)
book1.damaged_copies = 2
book2 = Book("Advanced Data Structures", 60.0, 5)
book2.damaged_copies = 1
Зараз атрибут damaged_copies
не інтегрований належним чином у клас Book
, оскільки він не прив'язаний до self
.
Додавати метод для обчислення кількості непошкоджених копій безпосередньо в класі Book
не є ідеальним, оскільки ця функціональність є специфічною для друкованих книг і не є релевантною для інших типів. Тому ми створимо клас PrintedBook
.
Дитячий та батьківський клас
Коли ми хочемо створити новий клас, подібний до класу Book
, з додатковою функціональністю, ми можемо використати наслідування, щоб уникнути дублювання методів і атрибутів класу Book
.
Дитячий клас успадковує від батьківського класу. Успадковуючи від батьківського класу, дитячий клас отримує всі методи та атрибути батька, водночас дозволяючи додавати нові функціональності, специфічні для дитячого класу.
class PrintedBook(Book):
all = []
def __init__(self, title: str, price: float, quantity=0, damaged_copies=0):
super().__init__(title, price, quantity)
assert (
damaged_copies >= 0
), f"Damaged copies {damaged_copies} is not greater or equal to zero"
self.damaged_copies = damaged_copies
PrintedBook.all.append(self)
Тут клас PrintedBook
успадковує від класу Book
, тому методи, як-от calculate_total_price
, доступні для екземплярів PrintedBook
.
Клас PrintedBook
вводить додатковий атрибут damaged_copies
, який є специфічним для класу PrintedBook
.
Покращення методу представлення
Створимо кілька екземплярів класу PrintedBook
, а потім виведемо всі екземпляри як класу Book
, так і PrintedBook
:
printed_book1 = PrintedBook("Introduction to Algorithms", 25, 7, 1)
printed_book2 = PrintedBook("Advanced Data Structures", 40, 3, 2)
print(Book.all)
print(PrintedBook.all)
Після виконання програми, результат виглядатиме так:
[Book('Introduction to Algorithms', 25, 7), Book('Advanced Data Structures', 40, 3)]
[Book('Introduction to Algorithms', 25, 7), Book('Advanced Data Structures', 40, 3)]
Виводяться екземпляри класу Book
, оскільки ми ще не реалізували метод __repr__
у класі Book
.
Щоб виправити це та динамічно відображати назву класу, ми можемо використати спеціальний магічний атрибут __class__.__name__
в методі __repr__
:
def __repr__(self):
return f"{self.__class__.__name__}('{self.title}', {self.price}, {self.quantity})"
Коли ми знову запустимо програму з цією зміною, результат стане таким:
[PrintedBook('Introduction to Algorithms', 25, 7), PrintedBook('Advanced Data Structures', 40, 3)]
[PrintedBook('Introduction to Algorithms', 25, 7), PrintedBook('Advanced Data Structures', 40, 3)]
Доступ до атрибутів і методів батьківського класу
Функція super()
є необхідною при роботі з наслідуванням. Вона дозволяє дочірньому класу доступати атрибути та методи батьківського класу без дублювання коду. Це гарантує, що будь-які зміни в батьківському класі автоматично поширюватимуться на дочірні класи. Наприклад:
super().__init__(title, price, quantity)
Якщо ми видалимо наступні рядки з класу PrintedBook
і знову запустимо програму, результат залишиться таким самим:
all = []
PrintedBook.all.append(self)
Це демонструє, що PrintedBook
безшовно успадковує функціональність від класу Book
, забезпечуючи чистий та підтримуваний код.
Організація коду в кількох файлах
Якщо наш проект росте, управління всім в одному файлі стає складним. Для вирішення цієї проблеми ми можемо розділити код на кілька файлів. Наприклад, можна створити окремі файли для класів Book
та PrintedBook
, а також файл main.py
, який буде відповідати за створення та управління екземплярами цих класів.
Ось як ми організуємо нові файли Python:
book.py
(перемістити класBook
та його імпорт сюди)printed_book.py
(перемістити класPrintedBook
та імпортуватиBook
сюди)
Оновимо main.py
, щоб створювати екземпляри та працювати з даними, ось так:
from book import Book
from printed_book import PrintedBook
Book.instantiate_from_csv()
print(Book.all)
Коли ви запустите main.py
, все працюватиме як очікується.
Геттери та Сеттери
Переваги використання геттерів і сеттерів полягають у тому, щоб надавати публічний доступ до даних і уникати порушення зовнішнього коду, коли ви змінюєте внутрішні поля. Розглянемо наступний код:
class Book:
def __init__(self, title: str):
self.title = title
book1 = Book("AAA")
book1.title = "BBB"
print(book1.title) # Output: BBB
Тут атрибут title
можна змінити після ініціалізації. Однак, ми можемо розглядати title
як критичний атрибут, тому нам може бути потрібно обмежити цю поведінку, щоб його можна було змінювати лише під час ініціалізації.
Геттери
Однією з проблем, яку ми маємо, є те, що атрибут title
потрібно зробити доступним лише для читання. Використовуючи декоратор @property
, ми можемо визначити атрибут, який буде тільки доступним і не змінюваним.
Створення атрибута тільки для читання
Щоб зробити атрибут title
доступним лише для читання, ми можемо визначити @property
у файлі book.py
таким чином:
class Book:
def __init__(self, title: str):
self.title = title
@property
def read_only_title(self):
return "AAA"
Тепер, в main.py
, якщо ми спробуємо доступити або змінити read_only_title
, ми побачимо таку поведінку:
from book import Book
book1 = Book("Introduction to Algorithms")
book1.title = "Algorithms"
print(book1.read_only_title) # Output: AAA
book1.read_only_title = "BBB" # Raises AttributeError
Як показано, атрибут read_only_title
зроблений доступним лише для читання. Це означає, що ви можете лише прочитати його значення, і спроба змінити його викликає AttributeError
.
Використання приватного атрибута для доступу тільки для читання
Декоратор @property
контролює тільки поведінку атрибута read_only_title
самостійно, а не те, як дані за ним змінюються. Тому ми можемо ввести приватний атрибут _title
в класі Book
.
Ось як це виглядає:
class Book:
def __init__(self, title: str):
self._title = title # Приватний атрибут з підкресленням
@property
def title(self):
return self._title
Цей підхід надає метод геттера (title
), щоб отримати значення атрибута _title
. Ось як це працює:
from book import Book
book1 = Book("Introduction to Algorithms")
print(book1.title) # Output: Introduction to Algorithms
print(book1._title) # Output: Introduction to Algorithms
book1.title = "Algorithms"
print(book1.title) # Raises AttributeError
book1._title = "Algorithms"
print(book1._title) # Output: Algorithms
Але є проблема: атрибут _title
можна змінювати напряму. Це суперечить меті мати властивість лише для читання, оскільки будь-хто може обійти властивість і змінити атрибут _title
безпосередньо.
Використання подвійних підкреслень для запобігання доступу
Щоб запобігти доступу або зміні атрибута ззовні класу, ми можемо додати подвійні підкреслення (__title
) до атрибута. Ось оновлений клас Book
:
class Book:
def __init__(self, title: str):
self.__title = title
@property
def title(self):
return self.__title
Тепер протестуємо це в main.py
:
from book import Book
book1 = Book("Introduction to Algorithms")
print(book1.title) # Output: Introduction to Algorithms
book1.title = "Algorithms"
print(book1.__title) # Raises AttributeError
Навіть спроба доступити __title
безпосередньо призводить до помилки. Це відбувається тому, що подвійні підкреслення роблять атрибут недоступним ззовні класу.
Сеттери
Раніше ми дізналися, що декоратор @property
робить атрибут лише для читання. Коли ми намагаємося встановити значення для такої властивості, виникає AttributeError
. Однак іноді нам потрібно дозволити змінювати атрибут, при цьому контролюючи, як і коли це можна зробити. Ось тут і допомагають сеттери.
Створення сеттера
Щоб створити сеттер для атрибута, спочатку визначеного як тільки для читання, ми визначаємо новий метод з такою самою назвою, як у властивості, і використовуємо декоратор @property_name.setter
. Цей декоратор дозволяє призначати нові значення для атрибута, навіть якщо це властивість.
Ось як ми визначаємо сеттер для атрибута title
:
class Book:
def __init__(self, title: str):
self.__title = title # Приватний атрибут для збереження title
@property
def title(self):
return self.__title
@title.setter
def title(self, value):
self.__title = value
Давайте подивимося, що трапиться, коли ми спробуємо встановити нове значення для атрибута title
у main.py
:
from book import Book
book1 = Book("myitem")
book1.title = "otherthing"
print(book1.title) # Output: otherthing
Тепер ми можемо не тільки встановлювати значення для приватного атрибута _title
через конструктор, але й призначати нове значення для атрибута title
пізніше, використовуючи сеттер.
Додавання валідації за допомогою сеттерів
Сеттери не лише для призначення нових значень; вони також можуть включати логіку для валідації або обмеження значень, які призначаються.
Наприклад, обмежимо атрибут title
максимальним розміром 10 символів:
class Book:
def __init__(self, title: str):
self.__title = title # Приватний атрибут
@property
def title(self):
return self.__title
@title.setter
def title(self, value):
if len(value) > 10:
raise Exception("Заголовок занадто довгий") # Обмеження: максимальна довжина 10
else:
self.__title = value
Давайте подивимося, як ця валідація впливає на поведінку програми в main.py
:
from book import Book
book1 = Book("short") # Ініціалізуємо з коротким заголовком
print(book1.title) # Output: short
book1.title = "acceptable" # Новий заголовок, довжина <= 10
print(book1.title) # Output: acceptable
book1.title = "this_title_is_too_long" # Raises Exception: Заголовок занадто довгий
Принципи ООП
Об'єктно-орієнтоване програмування (OOP) включає чотири основні принципи, які вам слід зрозуміти для ефективного проектування великих програм. Дотримуючись цих принципів, вам буде простіше підтримувати і розвивати ваші програми. Принципи:
1. Інкапсуляція
Інкапсуляція об'єднує атрибути та методи, які працюють з даними, в єдину одиницю, зазвичай клас, і обмежує прямий доступ до певних атрибутів. Наприклад, зробивши атрибут price
приватним у класі Book
, ми запобігаємо його прямому зміненню. Замість цього використовуються методи, як-от apply_discount
або apply_increment
, для зміни ціни, забезпечуючи, щоб виконувалися тільки валідні операції.
Ось початкова реалізація:
from book import Book
book1 = Book("Python Basics", 75)
book1.price = -90
print(book1)
У наведеному коді ви можете побачити, що ми можемо безпосередньо встановити атрибут price
на будь-яке значення, навіть на недійсне, як-от -90
.
Раніше ми використовували методи, як-от apply_discount
, щоб змінити атрибут price
, знижуючи його на 20%. Тепер, для кращого контролю, нам потрібно реалізувати методи, які обмежують доступ до атрибута price
. Ми можемо додати методи для збільшення ціни на відсоток або застосування знижки.
Оновлена реалізація:
class Book:
def __init__(self, price: float):
self.__price = price
@property
def price(self):
return self.__price
def apply_discount(self, discount_rate: float):
if 0 <= discount_rate <= 1:
self.__price = self.__price * (1 - discount_rate)
else:
raise ValueError("Рівень знижки має бути між 0 і 1.")
def apply_increment(self, increment_rate: float):
if increment_rate > 0:
self.__price = self.__price * (1 + increment_rate)
else:
raise ValueError("Рівень збільшення має бути додатним.")
Зробивши атрибут price
приватним (__price
), ми гарантуємо, що його можна змінювати лише через контрольовані методи, такі як застосування знижки або збільшення.
book1 = Book("Python Basics", 75)
print(book1.price) # Output: 75
book1.apply_discount(0.2)
print(book1.price) # Output: 60.0 (Застосовано знижку 20%)
book1.apply_increment(0.1)
print(book1.price) # Output: 66.0 (Застосовано збільшення 10%)
2. Абстракція
Абстракція зосереджується на наданні лише необхідних атрибутів і поведінки об'єкта, приховуючи непотрібні деталі. Це спрощує складність, надаючи спрощений інтерфейс, що робить системи легшими у використанні та розумінні.
Наприклад, розглянемо наступну програму:
from book import Book
book1 = Book("Python Basics", 75, 6)
book1.send_email()
У класі Book
метод send_email
є операцією високого рівня. Реальний процес включає кілька кроків у фоновому режимі, таких як підключення до SMTP сервера та підготовка тексту листа — ці деталі приховані від користувача. Щоб ефективно реалізувати абстракцію, ми можемо розбити ці кроки на окремі методи.
Наприклад, можемо створити метод для підключення до SMTP сервера:
class Book:
...
def connect(self, smtp_server):
pass
І метод для підготовки тіла електронного листа з автоматичним повідомленням:
class Book:
...
def prepare_body(self):
return f"""
Hello,
We have {self.quantity} of {self.title} available.
"""
Ці методи потім комбінуються в методі send_email
:
class Book:
...
def send(self):
pass
def send_email(self):
self.connect("")
self.prepare_body()
self.send()
У цьому підході методи, як-от connect
та prepare_body
, доступні безпосередньо з екземпляра класу, що порушує принцип абстракції. Щоб приховати ці деталі, Python дозволяє нам імітувати приватні методи, додаючи до їх назв подвійні підкреслення.
Зробивши ці методи приватними, ми забезпечуємо, щоб їх можна було викликати лише зсередини самого класу:
class Book:
...
def __connect(self, smtp_server):
pass
def __prepare_body(self):
return f"""
Hello,
We have {self.quantity} of {self.title} available.
"""
def __send(self):
pass
def send_email(self):
self.__connect("")
self.__prepare_body()
self.__send()
Якщо ви спробуєте звернутися до цих приватних методів безпосередньо з екземпляра в main.py
, вони не з’являться в автодоповненні або не будуть доступні. Це тому, що приватні методи призначені для використання лише всередині класу.
3. Спадкування
Спадкування дозволяє дочірньому класу успадковувати атрибути та методи від батьківського класу. Це сприяє повторному використанню коду, запобігає дублюванню і робить системи більш масштабованими та легшими для підтримки.
Наприклад, в цьому курсі ми реалізували спадкування, створивши клас PrintedBook
, який є нащадком класу Book
. Клас PrintedBook
успадковує всі атрибути та методи класу Book
:
from printed_book import PrintedBook
printed_book1 = PrintedBook("Python Basics", 75, 3)
Тут ми передаємо ті ж аргументи, що вимагає клас Book
, класу PrintedBook
. Спадкування також гарантує, що дочірні класи, такі як PrintedBook
, можуть отримати доступ до методів, визначених у батьківському класі.
Наприклад, метод apply_increment
визначений у класі Book
, але може бути використаний з екземпляром класу PrintedBook
:
from printed_book import PrintedBook
printed_book1 = PrintedBook("Python Basics", 75, 3)
printed_book1.apply_increment(0.2)
print(printed_book1.price)
Це демонструє, як логіка батьківського класу може бути повторно використана без переписування для кожного дочірнього класу, наприклад, для класів EBook
, AudioBook
або Article
.
4. Поліморфізм
Поліморфізм, що означає «багато форм», дозволяє об'єктам різних класів оброблятися так, ніби вони належать до одного спільного батьківського класу. Це досягається через перевизначення методів для надання специфічної поведінки, що забезпечує гнучкість і повторне використання коду.
Наприклад, вбудована функція Python len
демонструє поліморфізм, оскільки вона приймає різні типи об'єктів (як-от рядки, списки або словники) і повертає їх довжину відповідно:
name = "Sahar"
print(len(name)) # Output: 5
some_list = ["some", "name"]
print(len(some_list)) # Output: 2
Тут функція len
повертає кількість символів у рядку або кількість елементів у списку, демонструючи, як одна функція може обробляти кілька типів об'єктів. Поліморфізм також застосовується до власних методів класів. Розглянемо клас Book
і підклас PrintedBook
:
from printed_book import PrintedBook
printed_book1 = PrintedBook("Python Basics", 75, 3)
printed_book1.apply_discount()
print(printed_book1.price) # Output: 60.0
Метод apply_discount
визначений у класі Book
, але може бути використаний класом PrintedBook
, оскільки він успадковує від класу Book
.
Це демонструє поліморфізм: той самий метод працює з різними об'єктами.
Тепер давайте розширимо цей приклад з підкласом EBook
:
from book import Book
class EBook(Book):
discount_rate = 0.5
def __init__(self, title: str, price: float, quantity=0):
super().__init__(title, price, quantity)
Запуск коду показує, що метод apply_discount
в EBook
працює як очікується:
from ebook import EBook
ebook1 = EBook("Python Basics", 75, 3)
ebook1.apply_discount()
print(ebook1.price) # Output: 37.5
Тут метод використовує специфічну змінну discount_rate
, визначену в класі EBook
(discount_rate = 0.5
), що показує, як дочірні класи можуть налаштовувати поведінку, використовуючи той самий метод.
Поліморфізм стає ще потужнішим, коли поєднується з абстрактними класами, які виступають як шаблони для визначення спільних інтерфейсів між класами. Хоча абстрактні класи ще більше покращують поліморфізм, навіть без них поліморфізм є необхідним для написання гнучкого та зручного для підтримки коду на Python.
Перекладено з: Python Object Oriented Programming (OOP)