Polymorphism - полиморфизм

Полиморфизм в объектно-ориентированном программировании – это возможность обработки разных типов данных, т.е. принадлежащих к разным классам, с помощью «одно и той же» функции, или метода. На самом деле одинаковым является только имя метода, его исходный код зависит от класса. Кроме того, результаты работы одноименных методов могут существенно различаться. Поэтому в данном контексте под полиморфизмом понимается множество форм одного и того же слова – имени метода.

Например, два разных класса содержат метод total, однако инструкции каждого предусматривают совершенно разные операции. Так в классе T1 – это прибавление 10 к аргументу, в T2 – подсчет длины строки символов. В зависимости от того, к объекту какого класса применяется метод total, выполняются те или иные инструкции:

class T1:
     n=10
     def total(self, N):
          self.total = int(self.n) + int(N)

class T2:
     def total(self,s):
          self.total = len(str(s))

t1 = T1()
t2 = T2()
t1.total(45)
t2.total(45)
print(t1.total)         # Вывод: 55
print(t2.total)         # Вывод: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Мы уже наблюдали полиморфизм между классами, связанными наследованием. У каждого может быть свой метод __init__() или square() или какой-нибудь другой. Какой именно из методов square() вызывается, и что он делает, зависит от принадлежности объекта к тому или иному классу.

Однако классы не обязательно должны быть связанны наследованием. Полиморфизм как один из ключевых элементов ООП существует независимо от наследования. Классы могут быть не родственными, но иметь одинаковые методы, как в примере выше.

Полиморфизм дает возможность реализовывать так называемые единые интерфейсы для объектов различных классов. Например, разные классы могут предусматривать различный способ вывода той или иной информации объектов. Однако одинаковое название метода вывода позволит не запутать программу, сделать код более ясным.

В Python среди прочего полиморфизм находит отражение в методах перегрузки операторов. Два из них мы уже рассмотрели. Это __init__() и __del__(), которые вызываются при создании объекта и его удалении. Полиморфизм у методов перегрузки операторов проявляется в том, что независимо от типа объекта, его участие в определенной операции, вызывает метод с конкретным именем. В случае __init__() операцией является создание объекта.

Рассмотрим пример полиморфизма на еще одном методе, который перегружает функцию print().

Если вы создадите объект собственного класса, а потом попробуете вывести его на экран, то получите информацию о классе объекта и его адрес в памяти. Такое поведение функции print() по-умолчанию по отношению к пользовательским классам запрограммировано на самом верхнем уровне иерархии, где-то в суперклассе, от которого неявно наследуются все остальные:

class A:
    def __init__(self, v1, v2):
            self.field1 = v1
            self.field2 = v2

a = A(3, 4)
print(a)            # <__main__.A object at 0x7f840c8acfd0>
1
2
3
4
5
6
7

Если же мы хотим, чтобы, когда объект передается функции print(), выводилась какая-нибудь другая более полезная информация, то в класс надо добавить специальный метод str(). Этот метод должен обязательно возвращать строку, которую будет выводить функция print():

class A:
    def __init__(self, v1, v2):
        self.field1 = v1
        self.field2 = v2
    def __str__(self):
        return str(self.field1) + " " + str(self.field2)

a = A(3, 4)
print(a)            # 3 4
1
2
3
4
5
6
7
8
9

Какую именно строку возвращает метод str(), дело десятое. Он вполне может строить квадратик из символов:

class Rectangle:
    def __init__(self, width, height, sign):
        self.w = int(width)
        self.h = int(height)
        self.s = str(sign)

    def __str__(self):
        rect = []
        for i in range(self.h): # количество строк
            rect.append(self.s * self.w) # знак повторяется w раз
        rect = '\n'.join(rect) # превращаем список в строку
        return rect

b = Rectangle(10, 3, '*')
print(b)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Вывод в консоль:

**********
**********
**********
1
2
3

Полиморфизм является еще одним базовым аспектом объектно-ориентированного программирования и предполагает способность к изменению функционала, унаследованного от базового класса.

Например, пусть у нас будет следующая иерархия классов:

class Person:
    def __init__(self, name, age):
        self.__name = name  # устанавливаем имя
        self.__age = age  # устанавливаем возраст

    @property
    def name(self):
        return self.__name

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, age):
        if age in range(1, 100):
            self.__age = age
        else:
            print("Недопустимый возраст")

    def display_info(self):
        print("Имя:", self.__name, "\tВозраст:", self.__age)

class Employee(Person):
    # определение конструктора
    def __init__(self, name, age, company):
        Person.__init__(self, name, age)
        self.company = company

    # переопределение метода display_info
    def display_info(self):
        Person.display_info(self)
        print("Компания:", self.company)


class Student(Person):
    # определение конструктора
    def __init__(self, name, age, university):
        Person.__init__(self, name, age)
        self.university = university

    # переопределение метода display_info
    def display_info(self):
        print("Студент", self.name, "учится в университете", self.university)

people = [Person("Tom", 23), Student("Bob", 19, "Harvard"), Employee("Sam", 35, "Google")]

for person in people:
    person.display_info()
    print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

В производном классе Employee, который представляет служащего, определяется свой конструктор. Так как нам надо устанавливать при создании объекта еще и компанию, где работает сотрудник. Для этого конструктор принимает четыре параметра: стандартный параметр self, параметры name и age и параметр company.

В самом конструкторе Employee вызывается конструктор базового класса Person. Обращение к методам базового класса имеет следующий синтаксис:

суперкласс.название_метода(self [, параметры])
1

Поэтому в конструктор базового класса передаются имя и возраст. Сам же класс Employee добавляет к функционалу класса Person еще один атрибут - self.company.

Кроме того, класс Employee переопределяет метод display_info() класса Person, поскольку кроме имени и возраста необходимо выводить еще и компанию, в которой работает служащий. И чтобы повторно не писать код вывода имени и возраста здесь также происходит обращение к методу базового класса - методу get_info: Person.display_info(self).

Похожим образом определен класс Student, представляющий студента. Он также переопределяет конструктор и метод display_info за тем исключением, что вместо в методе display_info не вызывается версия этого метода из базового класса.

В основной части программы создается список из трех объектов Person, в котором два объекта также представляют классы Employee и Student. И в цикле этот список перебирается, и для каждого объекта в списке вызывается метод display_info. На этапе выполнения программы Python учитывает иерархию наследования и выбирает нужную версию метода display_info() для каждого объекта. В итоге мы получим следующий консольный вывод:

Имя: Tom    Возраст: 23
Студент Bob учится в университете Harvard
Имя: Sam    Возраст: 35
Компания: Google
1
2
3
4

Проверка типа объекта

При работе с объектами бывает необходимо в зависимости от их типа выполнить те или иные операции. И с помощью встроенной функции isinstance() мы можем проверить тип объекта. Эта функция принимает два параметра:

isinstance(object, type)
1

Первый параметр представляет объект, а второй - тип, на принадлежность к которому выполняется проверка. Если объект представляет указанный тип, то функция возвращает True. Например, возьмем выше описанную иерархию классов:

for person in people:
    if isinstance(person, Student):
        print(person.university)
    elif isinstance(person, Employee):
        print(person.company)
    else:
        print(person.name)
1
2
3
4
5
6
7

Вывод в консоль:

Tom
Harvard
Google
1
2
3

Упражнения

  1. Написать класс стол который принимает параметры: высота, ширина, высота, тип (круглый или квадратный).

  2. Написать два класса «круглый стол» и «прямоугольный стол» на следующих основной класс «Стол». И добавить в новые классы методы расчёта площади столешницы по соответствующим формулам.

  3. Написать один класс «Стол» включающим поле «форма столешницы» с методом «площадь столешницы», который вычисляет площадь в зависимости от выбранной формы.

  4. Написать интерфейс программы для столов содержащий, используя генерацию интерфейса:

    • метки и текстовые поля для вода данных;
    • кнопку при нажатии на которую в нижнее текстовое поле выводиться площадь столешницы.

Пример интерфейса:

app