Класи та об’єкти на Python
Це фрагмент книги Python з нуля, яка допоможе вам навчитися програмуванню з нуля. Ви можете знайти його на Allegro, Empik та в інтернет-книгарнях.
Оскільки ми переходимо до складніших тем, почнімо з ключових понять у мові Python: класи та об’єкти.
Якщо Ти на мить виглянеш за сторінки цієї книги, Ти, ймовірно, побачиш багато об’єктів. Я бачу ноутбук, чашку з кавою, нотатки, планшет для малювання... Наш світ наповнений об’єктами. Так само і в програмуванні — ми оперуємо об’єктами. Деякі з них дуже прості, наприклад рядки або логічні значення. Однак ми можемо визначати власні об’єкти. Це робиться за допомогою класів.
Визначення класів — дуже важливе, оскільки дозволяє нам думати про певні абстракції. В інтернет-магазині — це користувачі, продавці, товари тощо. У грі — гравці, противники та предмети. У медичному застосунку — пацієнти, лікарі, рецепти, направлення. Саме завдяки класам ми можемо виражати такі поняття та оперувати ними. Вони також визначають, як об’єкти всередині класів виглядатимуть і поводитимуться.
Клас — це свого роду шаблон для створення об’єктів. За аналогією, клас — це рецепт, а предмет — приготована страва. Клас визначає, що об’єкт повинен містити і як поводитися.
Почнемо з найпростішого варіанту, яким є порожній клас. Такий клас нічого не міститиме, але матиме назву, завдяки якій ми зможемо створювати об’єкти. Ми створюємо клас словом class
, після чого вказуємо назву класу, ставимо двокрапку та визначаємо тіло цього класу. Поки що ми визначаємо порожній клас, тому в його тілі ми розмістимо лише pass
.
Щоб створити за допомогою класу об’єкт, ми використовуємо назву класу та дужки. Це схоже на виклик функції, яка повертає окремий об’єкт. У прикладі нижче на нього буде вказувати змінна cookie
.
class Cookie:
pass
cookie = Cookie()
На цей момент наш клас порожній, тому об’єкт не дуже цікавий. Так буває далеко не завжди. Об’єкти можуть мати приписані їм змінні та функції. Вони називаються атрибутами, а функції в класі називаються методами. Незабаром ми про них поговоримо, але поки що почнемо з називання класів.
Називання класів
Називаючи класи, ми можемо використовувати ті ж символи, що і для змінних та функцій: малі й великі літери та символ підкреслення _
. Однак конвенція іменування дещо інша. Для функцій та змінних ми використовували snake_case. У випадку класів ми використовуємо PascalCase (або UpperCamelCase), тобто ми починаємо кожне слово з великої літери, не використовуючи пробіли та підкреслення.
Ось кілька прикладів класів із відповідними іменами:
User
;Invoice
;OrderReceipt
;NeuralNetwork
.
Вправа: Називання класів
Чи правильно названі наступні класи?
Personal_Invoice
;UserAddress
;Carengine
;doctor
;Dog
.
Відповіді в кінці книги.
Об’єктні змінні
Ми можемо призначити об’єкту змінну з певним значенням. Це значення стосуватиметься лише цього одного об’єкта. Щоб посилатися на змінну в об’єкті, ми повинні вказати і об’єкт, і змінну, розділивши їхні назви крапкою. Наприклад, для посилання на змінну type
в об’єкті cookie1
, ми використаємо cookie1.type
. Це застосовується і для присвоєння значення, і для його отримання.
class Cookie:
pass
cookie1 = Cookie()
cookie2 = Cookie()
cookie1.type = "Dog"
cookie1.breed = "Border Collie"
cookie2.type = "Food"
print(cookie1.type) # Dog
print(cookie1.breed) # Border Collie
print(cookie2.type) # Food
Об’єктні змінні рідко створюються поза класом, як у наведеному вище прикладі. Це навіть вважається поганою практикою. Вони частіше створюються у рамках методів, зокрема методу ініціалізатора. Ми дійдемо до цього крок за кроком.
Вправа: Класи і об’єктні змінні
Визнач клас Player
, який представлятиме гравців гри. Надай йому змінну points
зі значенням 0
. Виведи кількість балів (має складати 0
), а потім додай один бал до атрибута points
та виведи кількість балів ще раз (тепер вона має становити 1
).
Відповіді в кінці книги.
Методи
Усередині класів ми можемо визначати функції. Такі функції називаються методи. Ми визначаємо їх у тілі класу, а їхній перший параметр — це посилання на об’єкт, для якого ми будемо викликати цей метод. Цей параметр повинен називатися self
. Коли ми викликаємо метод, ми починаємо з об’єкта, потім ставимо крапку, назву методу та дужки з аргументами.
class User:
def cheer(self):
print(f"Привіт, мене звати {self.name}")
def say_hello(self, other):
name=self.name;
print(f"Привіт, {other}, мене звати {name}")
user = User()
user.name = "Мацєк"
user.cheer() # Привіт, мене звати Мацєк
user.say_hello("Марта")
# Привіт, Марта, мене звати Мацєк
Об’єкт self
можна використати також для зміни атрибутів даного об’єкта.
class Position:
def step_right(self):
self.x += 1.0
def move_up(self, value):
self.y += value
pos = Position()
pos.x = 0.0
pos.y = 0.0
pos.step_right()
print(pos.x) # 1.0
pos.move_up(6.0)
print(pos.y) # 6.0
pos.move_up(3.0)
print(pos.y) # 9.0
У наведених вище прикладах ми вказували атрибути об’єкта відразу після його створення. Такий підхід дуже небезпечний, адже користувач, наприклад, може забути визначити один із обов’язкових атрибутів. Натомість буде набагато краще, якщо ми встановимо значення цих атрибутів за допомогою конструктора.
Конструктор та ініціалізатор
При створенні нового об’єкта ми ставимо дужки після назви класу. Ця дужка — це виклик функції, яка формує об’єкт і називається конструктором. Ця функція виконує ряд кроків, необхідних для створення об’єкта, включаючи виклик спеціального методу __init__
1 з нашого класу. Цей метод називається ініціалізатором. У його тілі ми визначаємо, що має статися під час створення об’єкта. Найчастіше тут ми вказуємо атрибути об’єкта.
class Game:
def __init__(self):
print("Starting...")
self.started = True
game = Game() # Starting...
print(game.started) # True
Кількість параметрів функції __init__
визначає, скільки аргументів має входити до виклику конструктора (тобто в дужках, які ми ставимо після назви класу, коли створюємо об’єкт). Отже, якщо у функції __init__
ми додамо параметр name
, тоді під час створення об’єкта ми більше не зможемо залишати порожні дужки. Ми повинні вказати там аргумент, який буде назвою. Характерна риса функції __init__
— це те, що вона очікує певних параметрів, а потім призначає їх об’єкту як атрибути з тими ж назвами.
class User:
def __init__(self, name):
self.name = name
user1 = User("Мацєк")
user2 = User("Марта")
print(user1.name) # Мацєк
print(user2.name) # Марта
Ініціалізатор може містити багато параметрів. Ми можемо використовувати їх як завгодно для визначення атрибутів, хоча найчастіше значення параметрів встановлюються безпосередньо для однойменних атрибутів, як у випадку з name
і surname
у прикладі нижче. Значення атрибута full_name
розраховується на основі name
і surname
. Значення points
визначається як 0
.
class Player:
def __init__(self, name, surname):
self.name = name
self.surname = surname
self.full_name = f"{name} {surname}"
self.points = 0
player = Player("Міхал", "Мазур")
print(player.name) # Міхал
print(player.surname) # Мазур
print(player.full_name) # Міхал Мазур
print(player.points) # 0
Вправа: Банківський рахунок
Створи клас, який представляє банківський рахунок. Яка назва класу буде вдалою? Цей клас повинен мати атрибут balance
, який вказує суму коштів на цьому рахунку. Він також повинен містити методи:
deposit
, який додає вказану суму грошей доbalance
,withdraw
, який за достатньої кількості коштів віднімає суму відbalance
, і повертаєTrue
, а в інакшому випадку —False
.
account = BankAccount()
print(account.balance) # 0
account.deposit(1000)
print(account.balance) # 1000
account.deposit(2000)
print(account.balance) # 3000
res = account.withdraw(1500)
print(res) # True
print(account.balance) # 1500
res = account.withdraw(2000)
print(res) # False
print(account.balance) # 1500
Крім того, створи два об’єкти, які представляють банківські рахунки, й перевір, чи зміниться баланс одного з них, якщо Ти покладеш або знімеш гроші з іншого.
Відповіді в кінці книги.
Об’єкти та змінні
Тут я хочу підкреслити, що кожен об’єкт є окремою сутністю. Те, що вони мають схожий вигляд, не означає, що вони впливають одне на одного. Тому в наведеному нижче прикладі змінна name
в об’єкті user1
не матиме жодного впливу на user2
.
class User:
def __init__(self, name):
self.name = name
user1 = User("Рафал")
user2 = User("Рафал")
print(user1.name) # Рафал
print(user2.name) # Рафал
user1.name = "Бартек"
print(user1.name) # Бартек
print(user2.name) # Рафал
З іншого боку, якщо у нас є дві змінні, які вказують на один об’єкт, ми можемо змінити його, використовуючи будь-яку з них. Після цього зміняться значення обох змінних, тому що змінилося щось, на що ці дві змінні вказують.
user1 = User("Рафал")
user2 = user1
print(user1.name)
# Рафал
print(user2.name)
# Рафал
user1.name = "Бартек"
print(user1.name)
# Бартек
print(user2.name)
# Бартек
Це можна порівняти з прикладом, коли два об’єкти мали однакове значення, а потім змінилося те, на що вказує одна зі змінних. Результат буде іншим.
user1 = User("Рафал")
user2 = user1
print(user1.name)
# Рафал
print(user2.name)
# Рафал
user1 = User("Бартек")
print(user1.name)
# Бартек
print(user2.name)
# Рафал
user1
починає вказувати на інший.Приватні елементи
У Python є домовленість, що якщо ми не хочемо, щоб атрибути та методи використовувалися поза межами цього класу, їхні назви починаються з символу підкреслення _
. Такі атрибути та методи ми називаємо приватними. Вони повинні використовуватися виключно в інших методах того ж класу.
class BankAccount:
# ...
def _validate_user(self, token):
# ...
pass
def make_transaction(self, token, transaction):
self._validate_user(token)
# ...
Технічно такі елементи всеодно будуть доступні, проте слід уникати їх використання поза класом. Є багато причин робити певні атрибути приватними. Наприклад, у класі BankAccount
з попередньої вправи ми можемо захотіти стежити, щоб баланс рахунку ніколи не падав нижче нуля. Ми могли би зробити це, зробивши balance
приватним, а також повертаючи його значення методом get_balance
, а в методі withdraw
— контролюючи стан рахунку.
Атрибути класу
Іноді ми хочемо створити змінну або функцію, яка пов’язана не з об’єктом, а з класом. Іншими словами, вона буде спільною для всіх об’єктів цього класу.
Щоб створити змінну класу, достатньо визначити її в тілі цього класу. Щоб послатися на неї, ми спочатку використовуємо назву класу, потім ставимо крапку, а потім вказуємо назву цієї змінної. Тому не потрібно створювати жодних об’єктів.
class Score:
points = 0
print(Score.points) # 0
Score.points = 1
print(Score.points) # 1
Але тут на нас чекає сюрприз. Таким методом ми також можемо запитувати об’єкт, і він поверне своє значення. Є умова: не повинно бути об’єктної змінної з таким же ім’ям. Це означає, що коли у прикладі нижче ми вперше запитуємо score1.points
, то отримуємо значення змінної класу, а коли вдруге — об’єктну змінну.
class Score:
points = 0
score1 = Score()
print(score1.points) # 0
score1.points = 10
print(score1.points) # 10
Однак і тут є пастка. Коли хтось змінить значення змінної класу, усі об’єкти, які не мають об’єктної змінної з такою ж назвою, повертатимуть нові значення.
score1 = Score()
score1.points = 10
score2 = Score()
score3 = Score()
print(score1.points) # 10
print(score2.points) # 0
print(score3.points) # 0
Score.points = 2
print(score1.points) # 10
print(score2.points) # 2
print(score3.points) # 2
Іншими словами, якщо ми визначили атрибути з однаковими назвами для класу та об’єкта, тоді першими зчитуватимуться атрибути об’єкта, а якщо їх немає — з класу. Коли після об’єкта ми вказуємо назву змінної та використовуємо символ присвоєння, ми встановлюємо значення об’єктної змінної, а не змінної класу.
Змінні класу часто використовуються для зберігання значень, які не повинні змінюватися. Наприклад, клас, який обчислює розмір податку, може визначатися сталими значеннями податкових ставок.
class TaxCalculator:
VAT = 0.23
# ...
Зверни увагу, що я використав лише великі літери. У випадку сталих значень, тобто значень, встановлених розробником, які не змінюються, використовуються великі літери, тобто запис, відомий як SCREAMING_SNAKE_CASE.
Іноді утворюються класи виключно для зберігання сталих значень. Наприклад, у третій частині, присвяченій написанню гри "Змійка", нам потрібно буде якимось чином визначити напрямок руху. Для цього ми будемо використовувати клас Direction
зі змінними, які відповідатимуть за окремі напрямки. Ці змінні можна було б визначити інакше, але такий спосіб забезпечує нам чіткість.
class Direction:
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
# Приклад використання
direction = Direction.UP
if direction == Direction.UP:
direction = Direction.DOWN
Функції, присвоєні класу, а не об’єкту, називаються статичними. Вони визначаються так само, як звичайні методи, але не мають параметра self
. Ми також повинні ставити перед ними @staticmethod
. Ми можемо викликати ці методи в класі, вони не потребують об’єкта.
class Counter:
num = 0
def __init__(self):
print("Створюю")
Counter.num += 1
@staticmethod
def print_counter():
print(f"Створено {Counter.num}")
c1 = Counter() # Створюю
c2 = Counter() # Створюю
c3 = Counter() # Створюю
Counter.print_counter() # Створено 3
Перевірка класу об’єкта
Щоб перевірити, у якому класі створено об’єкт, ми можемо використовувати функцію type
, яка повертає об’єкт, що називається типом. Ми вже познайомилися із типами в розділі Основні значення, де розглянули str
, int
, float
і bool
. Назви класів — це також типи, тому ми можемо перевірити, чи об’єкт створено за допомогою класу Cookie
, порівнюючи його тип із Cookie
. Типи порівнюються за допомогою слова is
. Ми вже познайомилися з ним, обговорюючи значення None
у розділі Змінні. Якщо ми хочемо перевірити, чи типи відрізняються одне від одного, ми використовуємо is not
.
class Cookie:
pass
c = Cookie()
print(type(c)) # <class ‘__main__.Cookie’>
print(type(c) is Cookie) # True
print(type(c) is not Cookie) # False
print(type(c) is int) # False
print(type(c).__name__) # Cookie
Клас str
У розділі Основні значення ми познайомилися з рядками, числовими та логічними значеннями. Вони також є об’єктами, тому мають клас, конструктор і методи. Зосередьмося на класі str
, який використовується для створення рядків. Назва str
не дуже типова для класів, але врешті-решт рядок — це особливе значення. Його конструктор дозволяє перетворювати об’єкти іншого типу в об’єкт класу str
3.
str1 = "AAA"
print(type(str1)) # <class ‘str’>
i = 10
print(type(i)) # <class ‘int’>
str2 = str(i)
print(type(str2)) # <class ‘str’>
b = True
print(type(b)) # <class ‘bool’>
str3 = str(b)
print(type(str3)) # <class ‘str’>
Коли ми передаємо функції print
об’єкт, який має інший тип, ніж str
, він замінюється за допомогою конструктора. Він також використовується f-рядками, наприклад, у нещодавно використаному f"Створено {Counter.num}"
.
Клас str
визначає певні методи, які ми можемо викликати для будь-якого об’єкта типу str
. Ось найважливіші з них:
upper
повертає текст із перетворенням усіх малих літер на великі;lower
повертає текст із перетворенням усіх великих літер на малі;capitalize
повертає текст, у якому перша літера замінюється великою;title
повертає текст, у якому перші літери кожного слова замінюються на великі;replace
повертає текст із заміною всіх повторень одного слова на інше.
name = "dOmInIkA sito"
print(name.upper()) # DOMINIKA SITO
print(name.lower()) # dominika sito
print(name.capitalize()) # Dominika sito
print(name.title()) # Dominika Sito
text = "Привіт, {name}, пишу тобі"
new_text = text.replace("{name}", "Міхал")
print(new_text) # Привіт, Міхал, пишу тобі
new_text = new_text.replace("тобі", "Тобі")
print(new_text) # Привіт, Міхал, пишу Тобі
Завершення
У цьому розділі ми познайомилися з класами. Це дуже важлива частина програмування мовою Python, тож класи будуть корисними для нас впродовж всієї книги. А тепер перейдімо до особливого і дуже корисного класу — списку.
"init" — скорочення від "initialization", тобто "ініціалізатор". На початку та в кінці цієї назви міститься по два підкреслення.
Елемент — це поняття, яке включає класи, змінні та функції.
Як ми переконаємося в розділі Оператори, це робиться за допомогою спеціального методу __str__
.