Основи об’єктно-орієнтованого програмування в Python

Python ua: Об’єктно-орієнтоване програмування

Вступ

Всі програми, які ми писали до цього були побудовані навколо функцій — блоків інструкцій, які маніпулюють даними. Такий підхід до створення програм називається процедурним програмуванням. Існує інший спосіб організації програмного коду, який об’єднує дані і функціональність всередині сутності, яку називають об’єктом. Це називається об’єктно-орієнтованою парадигмою програмування. Більшість часу ти можеш використовувати процедурне програмування, але при написанні великих програм краще використовувати саме цей підхід.

Класи і об’єкти — два головних аспекти ООП. Клас створює новий тип, тим часом як об’єкти — це екземпляри класу. За аналогією можна сказати, що змінна типу int є екземпляром (об’єктом) класу int.

 

Зауваження для програмістів на статично-типізованих мовах

Навіть змінні цілочислового типу вважаються об’єктами (класу int). На відміну від C++ і Java (до версії 1.5) де цілі числа — примітивний тип. Див. help(int).

 

Об’єкти можуть зберігати в собі якісь дані, використовуючи звичайні змінні, які належать об’єкту. Змінні, які належать об’єкту або класу також називаються полями. Використовуючи функції, які належать класу, об’єкти отримують певну функціональність. Такі функції наз. методами класу. Ця термінологія важлива, тому що вона допомагає розрізняти незалежні функції і змінні і ті що є частиною об’єкту або класу. Поля і методи також можна назвати атрибутами класу.

Поля поділяються на два типи — вони можуть належати кожному екземпляру/об’єкту класу або лише самому класу. Вони називаються змінними об’єкту і змінними класу відповідно.

Клас створюється за допомогою ключового слова class. Поля і методи класу перераховуються всередині блоку.

 

 

self

Методи класу мають лише одну характерну відмінність від звичайних функцій — вони повинні мати додаткову змінну яка має бути додана до списку параметрів ПЕРШОЮ, але значення цього параметру ти не вказуєш, Python сам надасть його, коли метод буде викликано. Ця особлива змінна посилається на сам об’єкт, і за домовленістю, має назву self.

Можна замінити self на щось інше, але краще цього не робити. Використання стандартного імені дає багато переваг — будь-хто без проблем розпізнає його, і навіть спеціалізовані IDE будуть тобі допомагати, якщо ти використовуєш self.

 

Зауваження для програмістів на C++/java/C#

Ім’я self еквівалентно вказівнику this в C++ і посиланню this в Java і C#.

 

Напевно тебе дивує як Python надає значення змінній self і чому тобі не потрібно цього робити. Наступний приклад все прояснить. Отже, ти маєш клас MyClass і екземпляр цього класу, який називається myobject. Коли ти викликаєш метод цього об’єкту як myobject.method(arg1, arg2), виклик методу автоматично конвертується Python’ом в MyClass.method(myobject, arg1, arg2) — ось і все для чого self призначено.

Це також означає, що для будь-якого методу є хоча б один аргумент — self. Якщо ж знайдеться метод без аргументів, то тобі все одно доведеться вказувати один. І це буде self.

 

Класи

Найпростіший клас наведено нижче:

#!/usr/bin/python
# Filename: simplestclass.py

class Person:
    pass # Порожній блок

p = Person() # створення p який належить класу Person
print(p)

Вивід:


$ python simplestclass.py
<__main__.Person object at 0x019F85F0>

Ми створили клас використовуючи інструкцію class. Після неї йде вкладений блок інструкцій який формує тіло класу. В даному випадку блок порожній на що вказує інструкція pass.

Потім ми створили об’єкт/екземпляр цього класу, використовуючи ім’я класу, що розташоване перед парними дужками. Для нашої перевірки ми просто вивели на екран тип змінної. Перевірка показала, що у нас є екземпляр класу Person в модулі __main__.

Зауваж, що адреса пам’яті комп’ютера, де зберігається об’єкт також виводиться на екран. На твоєму комп’ютері адреса може бути іншою, оскільки Python може зберегти об’єкт туди, де він знайде вільне місце.

 

Методи об’єктів

Ми вже обговорили те що класи і об’єкти можуть мати методи, які є звичайними функціями окрім, звичайно, змінної self. Зараз ми розберемо приклад:

#!/usr/bin/python
# Filename: method.py

class Person:
    def sayHi(self):
        print('Hello, how are you?')

p = Person()
p.sayHi()
# Вище наведений код також можна написати як Person().sayHi()

Виведе:

 
$ python method.py
Hello, how are you?

Як це працює?

В цій програмі ми побачили змінну self в дії. Зауваж, що метод sayHi не приймає параметрів, але все одно має ім’я self у визначенні функції.

 

Метод __init__

В Python’івських класах є багато методів, які мають спеціальне значення. Зараз ми побачимо яке значення має метод __init__.

Метод __init__ виконується кожен раз, коли створюється екземпляр класу. Даний метод корисний, коли потрібно щось ініціалізувати. Зверни увагу на подвійне нижнє підкреслювання на початку і в кінці імені.

Приклад:

#!/usr/bin/python
# Filename: class_init.py

class Person:
    def __init__(self, name):
        self.name = name
    def sayHi(self):
        print('Hello, my name is', self.name)

p = Person('Swaroop')
p.sayHi()
# Можна написати лаконічніше: Person('Swaroop').sayHi()

Вивід:

 
$ python class_init.py
Hello, my name is Swaroop

Як це працює:

В цій програмі ми визначили метод __init__, який може приймати, крім параметру self, іще один — name. Всередині методу __init__ ми просто створили нове ім’я name. Зверни увагу на те, що це дві різні змінні не зважаючи на те, що вони називаються однаково. Крапкова нотація дозволяє нам розрізняти їх.

Більш важливо те, що ми не викликаємо метод __init__ явно. Ми просто визначили функцію всередині класу.

Після всього ми нарешті можемо використати поле self.name в наших методах. Як у випадку з методом sayHi.

 

Змінні класу і об’єкту

Ми вже поговорили про функціональну частину класів і об’єктів (методи), тепер же почнемо вивчати ту частину, що стосується даних. Дані, іншими словами поля, це не що інше як звичайні змінні обмежені простором імен класів і об’єктів. Це означає, що ці змінні є чинними лише в контексті цих класів і об’єктів. Ось чому це називається простором імен.

Існує два типи полів — змінні класу і змінні об’єкту, які класифікуються на основі того класу чи об’єкту належить змінна.

Змінні класу є спільними — до них можна отримати доступ з усіх екземплярів класу. Існує всього одна копія змінної даного типу і тому, коли один об’єкт змінює її, цю зміну побачать інші об’єкти.

Змінні об’єкту не є спільними для всіх екземплярів класу і належать лише певному об’єкту. Кожен об’єкт має власну копію поля, навіть якщо назва поля в одному об’єкті співпадає з назвою поля в іншому. Наступний приклад допоможе краще це зрозуміти:

#!/usr/bin/python
# Filename: objvar.py

class Robot:
'''Представляє робота з ім’ям'''

# Змінна класу в якій вказується к-ть роботів
population = 0

def __init__(self, name):
'''Ініціалізація даних'''
self.name = name
print('(Initializing {0})'.format(self.name)

# Плюс один робот
Robot.population += 1

def __del__(self):
'''Я помираю'''
print('{0} is being destroyed!'.format(self.name))

Robot.population -= 1

if Robot.population == 0:
print('{0} was the last one.'.format(self.name))
else:
print('There are still {0:d} robots working.'.format(Robot.population))

def sayHi(self):
'''Greeting by the robot.

Yeah, they can do that.'''
print('Greetings, my masters call me {0}.'.format(self.name))

def howMany():
'''Prints the current population.'''
print('We have {0:d} robots.'.format(Robot.population))
howMany = staticmethod(howMany)

droid1 = Robot('R2-D2')
droid1.sayHi()
Robot.howMany()

droid2 = Robot('C-3PO')
droid2.sayHi()
Robot.howMany()

print("\nRobots can do some work here.\n")

print("Robots have finished their work. So let's destroy them.")
del droid1
del droid2

Robot.howMany()

Вивід:

 
(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.

Robots can do some work here.

Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed!
There are still 1 robots working.
C-3PO is being destroyed!
C-3PO was the last one.
We have 0 robots.

Як це працює:

Це довгий приклад, але він допоможе проілюструвати сутність змінних класів і об’єктів. Змінна population належить класу Robot і таким чином є змінною класу. Змінна name належить об’єкту (привласнена за допомогою self) і тому є змінною об’єкту.

Отже, ми посилаємося на змінну класу population за доп. виразу Robot.population а не self.population. Але для доступу до змінної name,яка належить об’єкту, потрібно написати self.name всередині методу об’єкта. Запам’ятай цю просту відмінність між цими двома типами змінних. Також пам’ятай, що змінна об’єкту з тим же іменем що і змінна класу зробить недоступною (приховає) змінну класу!

howMany насправді метод класу. Це означає, що ми можемо визначити його або як classmethod або як staticmethod в залежності від того чи потрібно нам знати частиною якого класу він є. Оскільки ця інформація нам непотрібна, то використаємо staticmethod.

Ми б могли досягти того ж самого скориставшись декораторами (http://www.ibm.com/developerworks/linux/library/l-cpdecor.html):

n

@staticmethod
def howMany():
    '''Prints the current population.'''
    print('We have {0:d} robots.'.format(Robot.population))

Зауваж, що метод __init__ був використаний для створення екземпляру класу Robot і одночасно з цим наданням йому імені. В цьому методі ми збільшуємо змінну population на 1 кожен раз, коли викликається даний метод — кожен виклик це ще один робот. Значення self.name буде специфічним для кожного створеного нами об’єкту.

Запам’ятай, що звертатися до змінних і методів об’єкту з яким в даний момент працюєш потрібно використовуючи змінну self. Це називається посиланням на атрибут.

В цій програмі використані рядки документації (Коментарі обмежені 3 лапками). Ми можемо отримати доступ до рядків документації на рівні класу скориставшись конструкцією Robot.__doc__ і Robot.sayHi.__doc__ для документації, яка знаходиться в методі.

Крім методу __init__ існує іще один спеціальний метод __del__, який викликається кожного разу, коли об’єкт знищується. Іншими словами, його більше не використовують і ту область пам’яті, яку він займав звільняють. Всередині цього методу ми просто зменшуємо змінну Robot.population на 1 кожного разу, коли видаляємо об’єкт класу Robot.

Метод __del__ викликається тоді, коли об’єктом вже не користуються і зауваж, немає жодної гарантії коли саме це станеться. Можна і самостійно викликати цей метод виконавши інструкцію del для об’єкту. В такому випадку ми вручну знищимо об’єкт.

 

Зауваження для програмістів на C++, Java, C#

В Python всі члени класу є публічними, а всі методи віртуальними.

Один виняток: імена з 2-ма символами підкреслення в якості префіксу, такі як, наприклад, __privatevar. Змінні з такими ідентифікаторами таки стають приватними.

Таким чином існує домовленість, про те що всі імена змінних, які будуть використовуватися тільки всередині класу чи об’єкту мають починатися із символу нижнього підкреслення, а всі інші імена публічні, і можуть використовуватися іншими класами/об’єктами. Запам’ятай, що це тільки домовленість і Python не примушує тебе слідувати їй (окрім подвійного нижнього підкреслення).

 

 

Наслідування (успадкування)

Одна з головних вигод від використання ООП це повторне використання коду. І один із способів за допомогою якого це досягається полягає у використані механізму наслідування. Наслідування можна уявити як втілення відносин тип і підтип, між класами.

Припустимо, що ти хочеш написати програму, яка має слідкувати за вчителями і учнями в коледжі. Вони всі мають певні спільні характеристики такі як ім’я, вік і адреса. Також є певні специфічні ознаки такі як зарплатня, курси і плани для вчителів і оцінки та збори для учнів.

Ти можеш створити два незалежні класи для кожного типу, але додавання нової спільної характеристики буде означати додавання її до кожного класу з цих двох незалежних. Це швидко стане незручним.

Кращим підходом буде створення спільного класу SchoolMember з подальшим наслідуванням від цього класу двох інших класів — учень і вчитель. Ці класи стануть підтипами типу (класу) SchoolMember. В подальшому можна буде додавати будь-які специфічні для цих класів характеристики.

Цей метод має багато переваг. При додаванні/зміні функціональності класу SchoolMember зміни автоматично будуть відображені і у всіх його нащадках (підтипах, підкласах). Наприклад, можна додати до класу SchoolMember нове поле ID (ідентифікатор) картки, то підкласи вчитель і учень теж його отримають. Але зміни в підкласах не розповсюджуються на інші підкласи. Іншою перевагою є те що можна зіслатися на об’єкт вчитель або учень скориставшись об’єктом SchoolMember. Це може бути корисним в певних ситуаціях, коли наприклад, треба порахувати к-ть членів школи. Це називається поліморфізмом, коли підтип може, у будь-яких ситуаціях, в яких очікується використання батьківського типу, замістити собою цей самий батьківський тип. Іншими словами, об’єкт може бути опрацьований як примірник батьківського класу.

Також зауваж, що ми повторно використали код головного класу і нам не потрібно повторювати його в інших класах як у випадку з незалежними класами.

Клас SchoolMember в цій ситуації відомий як базовий клас або суперклас (надклас). Вчитель і учень — це похідні класи або підкласи.

Зараз подивимося як це виглядає в програмному коді:

#!/usr/bin/python
# Filename: inherit.py

class SchoolMember:
    ''' Призначений для представлення будь-якого члена школи '''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Initialized SchoolMember: {0})'.format(self.name))

    def tell(self):
        '''Показати деталі'''
        print('Name:"{0}" Age:"{1}"'.format(self.name, self.age), end="")

class Teacher(SchoolMember): # зверни увагу на те як ми наслідуємо від базового класу SchoolMember
    '''Представляє вчителя'''
    def __init__(self, name, age, salary): # даний методу буде викликано при створенні об’єкту
        SchoolMember.__init__(self, name, age) # виклик спеціального методу __init__ базового класу
        self.salary = salary
        print('(Initialized Teacher: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self) # тут також викликається метод tell базового класу
        print('Salary: "{0:d}"'.format(self.salary))

class Student(SchoolMember): # тут так само як і у випадку з класом Teacher
    '''Представляє учня'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(Initialized Student: {0})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Marks: "{0:d}"'.format(self.marks))

t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

print() # виведе порожній рядок

members = [t, s]
for member in members:
    member.tell() # працює як для учня так і для викладача

Вивід:

 
$ python inherit.py
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: Swaroop)
(Initialized Student: Swaroop)

Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"

Як це працює:

Для використання наслідування ми вказали ім’я базового класу (від якого будемо наслідувати) в дужках які йдуть відразу після імені класу (як і при створенні функцій, лише з тією різницею, що в якості аргументу виступає назва базового класу). Приклад:

class Student(SchoolMember):

Далі відбувається виклик методу __init__ базового класу якому передається посилання на поточний екземпляр класу, тобто об’єкт (який буде створено в майбутньому). Це робиться для того, щоб ми могли ініціалізувати базовий клас як частину об’єкта. Дуже важливо запам’ятати, що Python автоматично не викликає конструктор базового класу, тобі потрібно самостійно викликати його.

Зауваж, те як ми викликали метод базового класу з його підкласу:

SchoolMember.__init__(self, name, age) # звісно ж, тут може бути будь-яке ім’я класу і будь-який метод з потрібними тобі параметрами

Слід знати, що Python завжди починає з пошуку методів всередині поточного класу, якщо ж він не може знайти потрібного методу всередині підкласу, то починає шукати в базовому класі. В тому порядку в якому вони (методи) розташовані у визначенні класу.

Якщо відбувається наслідування від декількох класів (більше ніж один клас перераховано в визначенні підкласу), то це називається множинним наслідуванням.

 

Підсумок

Python дуже об’єктно-орієнтована мова і добре розуміння концепцій ООП дуже тобі допоможе в довгостроковій перспективі.

 

Далі ми дізнаємося як працювати з файлами.

Якщо у вас виникли питання або щось не зрозуміло - залишайте коментарі і ми дамо відповідь!