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

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

uk-flagанглійська: Object Oriented Programming

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

Класи та об’єкти є двома основними аспектами об’єктно-орієнтованого програмування. Клас (англ.“class”) створює новий тип (англ.“type”), тим часом як об’єкти (англ.“objects”) є екземплярами (англ.“instances”) класу. Аналогічно,коли ми говоримо про змінні типи int, це означає, що змінні, які зберігають цілочисельні значення, є екземплярами (об’єктами) класу int.


Примітка для програмістів статичної мови

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

Перегляньте help(int) для отримання додаткової інформації про клас.

Програмісти C# і Java 1.5 знайдуть це схожим з концепцією упаковки та распаковки (англ.» boxing та unboxing»).


Об’єкти можуть зберігати дані за допомогою звичайних змінних, які належать об’єкту. Змінні, які належать об’єкту або класу, називаються полями (англ.“fields”). Об’єкти також можуть мати функціонал, тобто мати функції, які належать до класу. Такі функції називаються методами (англ.“methods”) класу. Ця термінологія важлива, оскільки вона допомагає нам розрізняти незалежні функції і змінні,і ті, що належать до класу чи об’єкта. У сукупності поля та методи можна назвати атрибутами (англ.“attributess”) цього класу.

Поля бувають двох типів - вони можуть належати кожному екземпляру/об’єкту класа або вони можуть належати лише самому класу. Вони називаються змінними екземпляра (англ.“instance variables”) і змінними класа (англ.“class variables”) відповідно.

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

Self#

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

Незважаючи на те, що ви можете дати будь-яку назву для цього параметра, наполегливо рекомендовано використовувати назву self - будь-яка інша назва однозначно неприйнятна. Є багато переваг використання стандартної назви - будь-який читач вашої програми одразу впізнає її, і навіть спеціалізовані IDE (інтегровані середовища розробки) можуть допомогти вам, якщо ви використовуєте self.


Примітка для програмістів C++/Java/C#

self у Python еквівалентний вказівнику this у C++ і посиланню this у Java та C#.


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

Це також означає, що якщо у вас є метод, який не приймає аргументів, ви все одно повинні мати один аргумент – self.

Класи#

uk-flagанглійська: Classes

Найпростіший можливий клас показано в наступному прикладі (збережіть як oop_simplestclass.py).

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

p = Person()
print(p)

Висновок:


<__main__.Person object at 0x10171f518>

Як це працює

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

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

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

Методи#

Ми вже обговорювали, що класи/об’єкти можуть мати методи, що являють собою функції, за винятком додаткової змінної self. Тепер ми побачимо приклад (збережіть як oop_method.py).

код python oop1_ukr.py

class Person:
    def скажи_привіт(self):
        print('Привіт,ти хто?')

p = Person()
p.скажи_привіт()

Висновок:

Привіт,ти хто?

Як це працює

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

Метод __init__#

uk-flagанглійська: The __init__ method

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

Метод __init__ запускається, як тільки об’єкт класу створюється (тобто реалізується). Цей метод корисний для будь-якої ініціалізації (тобто передачі початкових значень вашому об’єкту), яку ви хочете зробити з вашим об’єктом. Зверніть увагу на подвійне підкреслення як на початку, так і в кінці імені.

код python oop_init_ukr.py

class Person:
    def __init__(self, ім_я):
        self.ім_я = ім_я

    def скажи_привіт(self):
        print('Привіт,моє ім_я', self.ім_я)

p = Person('Swaroop')
p.скажи_привіт()
# Попередні 2 рядки також можна записати як
# Person().скажи_привіт()

Висновок:

Привіт,моє ім_я Swaroop

Як це працює

Тут ми визначаємо метод __init__ так, щоб він приймав параметр ім_я(разом із звичайним self). Тут ми просто створюємо нове поле, яке також називається ім_я. Зауважте, що це дві різні змінні, хоча обидві мають назву ‘ім_я’. Немає жодних проблем, оскільки нотація з крапкою self.ім_я означає, що існує щось під назвою “ім_я”, яке є частиною об’єкта під назвою “self”, а інше ім_я є локальною змінною. Оскільки ми чітко вказуємо, яке ім’я маємо на увазі, плутанини немає.

Створюючи новий екземпляр p класу Person, ми вказуємо ім’я класу, після якого- аргументи в дужках: p = Person(‘Swaroop’).

Ми явно не викликаємо метод __init__. У цьому полягає особлива значимість цього методу.

Тепер ми можемо використовувати поле self.ім_я у наших методах, що продемонстровано в методі скажи_привіт.

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

uk-flagанглійська: Class And Object Variables

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

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

Змінні класу (англ.“Class variables”) є спільними – до них можуть отримати доступ усі екземпляри цього класу. Існує лише одна копія змінної класу, і коли будь-який об’єкт вносить зміни до змінної класу, цю зміну побачать усі інші екземпляри.

Змінні об’єкту (англ.“Object variables”) не є спільними, змінні об’єкту належать кожному окремому об’єкту/екземпляру класу. У цьому випадку кожен об’єкт має власну копію поля, тобто вони не є спільними та жодним чином не пов’язані з полем з тим самим ім’ям в іншому екземплярі. Приклад допоможе зрозуміти це (збережіть як oop_objvar.py):

код python oop_objvar_ukr.py

class Робот:
    """Представляє робота з ім’ям."""

    # Змінна класу,яка підраховує кількість роботів
    населення = 0

    def __init__(self, ім_я):
        """Створення (initialization -ініціалізація) даних."""
        self.ім_я = ім_я
        print("(Ініціалізація {})".format(self.ім_я))

        # При створенні цієї особи, робот 
        # додається до змінної 'населення'
        Робот.населення += 1

    def вмирати(self):
        """Я вмираю."""
        print("{} знищується".format(self.ім_я))

        Робот.населення -= 1

        if Робот.населення == 0:
            print("{} я був останній.".format(self.ім_я))
        else:
            print("Все ще є {:d} робочий робот.".format(
                Робот.населення))

    def скажи_привіт(self):
        """Привітання від робота..

        Так, вони можуть це зробити."""
        print("Вітаю, мої господарі називають мене  {}.".format(self.ім_я))

    @classmethod
    def скільки(cls):
        """Друкує поточне населення."""
        print("У нас є {:d} робот.".format(cls.населення))


droid1 = Робот("R2-D2")
droid1.скажи_привіт()
Робот.скільки()

droid2 = Робот("C-3PO")
droid2.скажи_привіт()
Робот.скільки()

print("\nРоботи можуть виконувати тут певну роботу.\n")

print("Роботи закінчили свою роботу. Тож давайте їх знищимо.")
droid1.вмирати()
droid2.вмирати()
Робот.скільки()

Висновок:

(Створення R2-D2)
Вітаю, мої господарі називають мене  R2-D2.
У нас є 1 робот.
(Створення C-3PO)
Вітаю, мої господарі називають мене  C-3PO.
У нас є 2 робот.

Роботи можуть виконувати тут певну роботу.

Роботи закінчили свою роботу. Тож давайте їх знищимо.
R2-D2 знищується
Все ще є 1 робочий робот.
C-3PO знищується
C-3PO я був останній.
У нас є 0 робот.

Як це працює

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

Таким чином, ми звертаємося до змінної класу населення як Робот.населення, а не self.населення. До змінної ж об’єкта ім_я у всіх методах цього об’єкта ми звертаємося за допомогою позначення self.ім_я. Запам’ятайте цю просту різницю між змінними класу та об’єкта. Також зауважте, що змінна об’єкта з тим же іменем, що й змінна класу, приховає змінну класу!

Замість Робот.населення ми також могли б використати self.__class__.населення, оскільки кожен об’єкт посилається на свій клас через атрибут self.__class__.

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

Ми позначили метод скільки як метод класу за допомогою decorator.

Декоратори можна уявити як ярлик для виклику функції-обгортки (англ.“wrapper function”) (тобто функції, яка «обгортається» навколо іншої функції, щоб вона могла робити щось до або після внутрішньої функції), тому застосування декоратора @classmethod є таким самим, як і функція виклику:

скільки = classmethod(скільки)

Зверніть увагу, що метод __init__ використовується для ініціалізації екземпляра Робот з ім’ям. У цьому методі ми збільшуємо кількість населення на 1, оскільки ми додаємо ще одного робота. Також зауважте, що значення self.ім_я для кожного об’єкта свої, що свідчить про природу змінних об’єкта.

Пам’ятайте, що ви повинні звертатися до змінних і методів того самого об’єкта, використовуючи тільки self. Це називається посиланням на атрибут (англ.“attribute reference.”).

У цій програмі ми також бачимо використання рядків документації (англ.“docstrings”) для класів, а також для методів. Під час виконання ми можемо звертатись до рядка документації класу за допомогою Робот.__doc__ ,а до рядка документації методу – за допомогою Robot.скажи_привіт.__doc__

У методі вмирати ми просто зменшуємо кількість Робот.населення на 1.

Усі члени класу є відкритими. Один виняток: якщо ви використовуєте елементи даних з іменами, що використовують префікс подвійного підкреслення,(англ.»double underscore prefix «) наприклад __privatevar, Python використовує спотворення імен (англ.“name-mangling”), щоб ефективно зробити їх приватною змінною.

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

Примітка для програмістів C++/Java/C#

Усі члени класу (включно з членами даних) є загальнодоступними ( англ.“public”), а всі методи — віртуальними (англ.“virtual”) у Python.

Наслідування#

uk-flagанглійська: Inheritance

Однією з головних переваг об’єктно-орієнтованого програмування є багаторазове використання (англ.“reuse”) одного і того ж коду, і один із способів цього досягти - за допомогою механізму наслідування (англ. “inheritance”). Наслідування найкраще можна уявити у вигляді відношення між класами як тип та підтип (англ.» type and subtype»).

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

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

Кращим способом було б створити загальний клас під назвою УчастникШколи, а потім зробити так, щоб класи викладача і студента успадкували цей клас, тобто вони стануть підтипами цього типу (класу), і тоді ми зможемо додати певні характеристики до цих підтипів -типів.

Цей підхід має багато переваг. Якщо ми додаємо/змінюємо будь-яку функціональність в УчастникШколи, це також автоматично відображається в підтипах. Наприклад, ви можете додати нове поле ідентифікаційної картки як для вчителів, так і для студентів, просто додавши його до класу УчастникШколи. Однак зміни в підтипах не впливають на інші підтипи. Ще одна перевага полягає в тому, що ви можете звертатися до об’єкта «вчитель» або «студент» як до об’єкта УчастникШколи, що може бути корисним у деяких ситуаціях, наприклад підрахунок кількості учасників школи. Це називається поліморфізмом (англ.“polymorphism”), коли підтип можна підставити у місці, де очікується батьківський тип, тобто об’єкт вважається екземпляром батьківського класу.

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

Клас УчастникШколу цій ситуації відомий як базовий клас (англ.“base class”) або суперклас (англ.» superclass»). Класи Вчитель і Студент називаються похідними класами (англ.“derived classes”) або підкласами (англ.» subclasses»).

Тепер ми побачимо цей приклад на англійській мові як програму (збережіть як oop_subclass.py):

код python oop_subclass_ukr.py

class УчастникШколи:
    '''Представляє будь-якого участника школи.'''
    def __init__(self, ім_я, вік):
        self.ім_я = ім_я
        self.вік = вік
        print('(Створено УчастникШколи: {})'.format(self.ім_я))

    def повідомити(self):
        '''Повідомити деталі.'''
        print('Ім_я:"{}" Вік:"{}"'.format(self.ім_я, self.вік), end=" ")


class Викладач(УчастникШколи):
    '''Представляє викладача.'''
    def __init__(self, ім_я, вік, зарплата):
        УчастникШколи.__init__(self, ім_я, вік)
        self.зарплата = зарплата
        print('(Створено Викладач: {})'.format(self.ім_я))

    def повідомити(self):
        УчастникШколи.повідомити(self)
        print('зарплата: "{:d}"'.format(self.зарплата))


class Студент(УчастникШколи):
    '''Представляє студента.'''
    def __init__(self, ім_я, вік, оцінки):
        УчастникШколи.__init__(self, ім_я, вік)
        self.оцінки = оцінки
        print('(Створено студент: {})'.format(self.ім_я))

    def повідомити(self):
        УчастникШколи.повідомити(self)
        print('Оцінки: "{:d}"'.format(self.оцінки))

t = Викладач('Mrs. Shrividya', 40, 30000)
s = Студент('Swaroop', 25, 75)

# друкує порожній рядок
print()

учасники = [t, s]
for учасники in учасники:
    # Працює як для вчителів, так і для студентів
    учасники.повідомити()

Висновок:

(Створено УчастникШколи: Mrs. Shrividya)
(Створено Викладач: Mrs. Shrividya)
(Створено УчастникШколи: Swaroop)
(Створено студент: Swaroop)

Ім_я:"Mrs. Shrividya" Вік:"40" зарплата: "30000"
Ім_я:"Swaroop" Вік:"25" Оцінки: "75"

python code oop_subclass_en.py

class SchoolMember:
    '''Represents any school member.'''
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('(Initialized SchoolMember: {})'.format(self.name))

    def tell(self):
        '''Tell my details.'''
        print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")


class Teacher(SchoolMember):
    '''Represents a teacher.'''
    def __init__(self, name, age, salary):
        SchoolMember.__init__(self, name, age)
        self.salary = salary
        print('(Initialized Teacher: {})'.format(self.name))

    def tell(self):
        SchoolMember.tell(self)
        print('Salary: "{:d}"'.format(self.salary))


class Student(SchoolMember):
    '''Represents a student.'''
    def __init__(self, name, age, marks):
        SchoolMember.__init__(self, name, age)
        self.marks = marks
        print('(Initialized Student: {})'.format(self.name))

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

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

# prints a blank line
print()

members = [t, s]
for member in members:
    # Works for both Teachers and Students
    member.tell()

output:

(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 Викладач (УчастникШколи). Далі ми спостерігаємо, що метод __init__ базового класу явно викликається за допомогою змінної self, щоб ми могли ініціалізувати частину об’єкта, що відноситься до базового класу. Це дуже важливо пам’ятати,оскільки ми визначаємо метод __init__ у підкласах Викладач та Студент, Python не викликає автоматично конструктор базового класу УчастникШколи, ви повинні викликати його самостійно у явному вигляді.

Навпаки, якщо ми не визначили метод __init__ у підкласі, Python автоматично викличе конструктор базового класу.

Хоча ми могли б обробляти екземпляри Викладач або Студент так само, як екземпляр УчастникШколи, і отримати доступ до методу повідомити класа УчастникШколи, просто ввівши Викладач.повідомити або Студент.повідомити.Натомість ми визначаємо інший метод повідомити у кожному підкласі (використовуючи метод повідомити в класі УчастникШколи для його частини), щоб пристосувати його для цього підкласу. Оскільки ми це зробили, написавши Викладач.повідомити, Python використовує метод повідомити для цього підкласу проти суперкласу. Однак, якби у нас не було методу повідомити у підкласі, Python використовував би метод повідомити у суперкласі. Python завжди спочатку починає шукати методи у фактичному типі підкласу, і якщо він нічого не знаходить, він починає шукати методи в базових класах підкласу, один за іншим у тому порядку, в якому вони вказані в кортежі (тут у нас є лише 1 базовий клас, але ви можете мати кілька базових класів) у визначенні класу.

Примітка щодо термінології: якщо в кортежі наслідування зазначено більше одного класу, це називається множинним наслідуванням (англ.“multiple inheritance”).

Параметр end використовується у функції print у методі повідомити() суперкласу, щоб надрукувати рядок і дозволити наступному друку продовжуватись у тому самому рядку. Це трюк, щоб змусити print не друкувати символ \n (новий рядок) у кінці друку.

Резюме#

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

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