Перейти к содержанию

Полиморфизм

После предыдущего занятия мы уже немного разбираемся в объектно-ориентированном программировании: освоили определение классов и методов, добавление атрибутов в объекты. Понятно, что классы, объекты, методы, атрибуты достаточно удобны и красивы, но в чем их преимущество перед функциями? Ведь некоторые объекты можно было бы передавать в функции и выполнять над ними те же действия, что и с помощью методов. Зачем Вводить дополнительный синтаксис и правила?

Основное преимущество в том, что объектно-ориентированный подход позволяет писать код, который будет работать с экземплярами различных классов. Иногда код может даже работать с классами, которые еще не созданы.

Свойство кода работать с разными типами данных называют полиморфизмом. Мы уже неоднократно пользовались этим свойством многих функций и операторов, не задумываясь о нем. Например, оператор + является полиморфным:

print(1 + 2)  # 3
print(1.5 + 0.2)  # 1.7
print("abc" + "def")  # abcdef

Внутренняя реализация оператора + существенно отличается для целых чисел, чисел с плавающей точкой и строк. То есть на самом деле это три разные операции — интерпретатор Python выбирает одну из них при выполнении в зависимости от операндов. Впрочем, в нашем случае выбор очевиден, потому что операнды — просто константы.

Усложним задачу:

def f(x, y):
    return x + y


print(f(1, 2))  # 3
print(f(1.5, 0.2))  # 1.7
print(f("abc", "def"))  # abcdef

Перехитрить интерпретатор не удалось, ведь Python — язык с динамической типизацией. В таких языках любое значение несет в себе информацию о типе — она и помогла интерпретатору выбрать правильную реализацию операции + (а заодно и правильное строковое представление для функции print).

Но мы знаем, что тип данных в Python — класс объекта, и именно эта информация о классе объекта используется при выборе операции. На следующем занятии мы вернемся к оператору + и рассмотрим, как реализовать его для наших собственных классов.

Давайте теперь вспомним про метод __init__. Он выполняется при создании каждого нового экземпляра класса и инициализирует свойства нового экземпляра. Первый аргумент, self, он получает от интерпретатора, остальные передаются классу в круглых скобках при создании экземпляра.

class Book:
    def __init__(self, name, author):
        self.name = name
        self.author = author


    def get_name(self):
        return self.name


    def get_author(self):
        return self.author


book = Book('Война и мир', 'Толстой Л. Н.')
print('{}, {}'.format(book.get_name(), book.get_author()))  # Война и мир, Толстой Л. Н.

При исполнении кода book = Book('Война и мир', 'Толстой Л. Н.') будет создан объект, у которого до момента присваивания ссылки на него переменной вызовется метод __init__, создающий атрибуты name, author и задающий им значения. Читать свойства можно из объекта напрямую (например, book.name) или использовать определенные для этого методы. Второй способ лучше, так как позволяет оградить программистов — пользователей класса от возможных изменений в реализации класса.

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

Посмотрим на реализацию классов "Круг" и "Квадрат" для подсчета площади и периметра:

from math import pi


class Circle:
    def __init__(self, radius):
        self.radius = radius


    def area(self):
        return pi * self.radius ** 2


    def perimeter(self):
        return 2 * pi * self.radius


class Square:
    def __init__(self, side):
        self.side = side


    def area(self):
        return self.side * self.side


    def perimeter(self):
        return 4 * self.side

Мы определили классы Circle и Square, экземпляры которых могут считать площадь и периметр окружностей и квадратов. Важно, что у обоих классов одинаковый интерфейс: методы для расчета площади называются area, а для расчета периметра — perimeter. Кроме того, у этих методов одинаковое количество параметров (в данном случае только self), и они оба возвращают в результате работы число, хотя оно и может быть разного типа (целое и вещественное).

Теперь мы можем определить полиморфную функцию print_shape_info, которая будет печатать данные о фигуре:

def print_shape_info(shape):
    print("Area = {}, perimeter = {}.".format(
        shape.area(), shape.perimeter()))


square = Square(10)
print_shape_info(square)  # Area = 100, perimeter = 40

print_shape_info(circle)  # Area = 314.1592653589793, perimeter = 62.83185307179586.

Если аргумент функции print_shape_info — экземпляр класса Square, выполняются методы, определенные в этом классе, если экземпляр Circle — методы Circle.

Утиная типизация

Утиная типизация

Данный код использует тот факт, что в Python принята так называемая утиная типизация. Название происходит от шутливого выражения:

Info

Если нечто выглядит как утка, плавает как утка и крякает как утка, это, вероятно, утка и есть.

В программах на Python это означает, что если какой-то объект поддерживает все требуемые от него операции, с ним и будут работать с помощью этих операций, не заботясь о том, какого он на самом деле типа. Так и наша функция print_shape_info будет Выводить информацию о любом объекте, у которого есть методы area и perimeter (и у которых в списке параметров также будет указан один параметр self).

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

Чтобы полиморфизм работал, за ними надо следить как на уровне синтаксиса (одинаковые имена методов и количество параметров), так и на уровне смысла (методы с одинаковыми именами делают похожие операции, параметры методов имеют тот же смысл).

Давайте определим еще один класс с таким же интерфейсом, как у Circle и Square, — например, Rectangle (прямоугольник). Если мы все сделаем правильно, функция print_shape_info сможет работать с его экземплярами:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height


    def area(self):
        return self.width * self.height


    def perimeter(self):
        return 2 * (self.width + self.height)


rect = Rectangle(10, 15)
print_shape_info(rect)  # Area = 150, perimeter = 50.

Еще раз обратите внимание: утиная типизация позволяет заранее написать функцию, которая будет работать со всеми экземплярами любых классов — даже еще не существующих. Важно лишь, чтобы эти классы поддерживали необходимый функции интерфейс.

И небольшое замечание об инкапсуляции. Дело в том, что с самого начала обычно есть не два класса, как в нашем примере, а один. Пусть это будет square. Если не инкапсулировать внутри него свойство side и не определить заранее интерфейс для расчета площади и периметра, никакого полиморфизма не получится. Важно помнить о том, что инкапсуляция определяет понятие интерфейса класса и создает базу для полиморфизма.

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

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

Эта функция принимает два параметра: isinstance(object, type); Первый параметр представляет объект, а второй — тип, на принадлежность к которому выполняется проверка. Если объект представляет указанный тип, функция возвращает True.

for person in people:
    if isinstance(person, Student):
        print(person.university)
    elif isinstance(person, Employee):
        print(person.company)
    else:
        print(person.name)

Задачи

Задача 1

Напишите класс Selector. Экземпляр этого класса при инициализации получает список чисел. Вызов метода get_odds возвращает нечётные числа из первоначального списка, вызов get_evens — чётные. Числа должны идти в том же порядке, в котором они были в изначальном списке.

Тест 1

Ввод

# Ваш код

values = [11, 12, 13, 14, 15, 16, 22, 44, 66]
selector = Selector(values)
odds = selector.get_odds()
evens = selector.get_evens()
print(' '.join(map(str, odds)))
print(' '.join(map(str, evens)))

Вывод

11 13 15
12 14 16 22 44 66

Тест 2

Ввод

# Ваш код

values = [6, 6, 0, 4, 8, 7, 6, 4, 7, 5]
selector = Selector(values)
odds = selector.get_odds()
evens = selector.get_evens()
print(' '.join(map(str, odds)))
print(' '.join(map(str, evens)))

Вывод

7 7 5
6 6 0 4 8 6 4

Тест 3

Ввод

# Ваш код

values = []
selector = Selector(values)
odds = selector.get_odds()
evens = selector.get_evens()
print(' '.join(map(str, odds)))
print(' '.join(map(str, evens)))

Вывод

# empty line
# empty line

Задача 2

Напишите два класса: LeftParagraph и RightParagraph для печати абзаца с выравниванием по левому и правому краю. При инициализации экземпляры обоих классов должны принимать целое число — ширину поля вывода. В обоих классах нужно реализовать метод add_word для добавления слова в абзац и метод end, выводящий полученный абзац на печать и начинающий формирование нового. Гарантируется, что длина любого слова меньше ширины поля вывода.

Тест

Ввод

# Ваш код

lp = LeftParagraph(8)
lp.add_word('abc')
lp.add_word('defg')
lp.add_word('hi')
lp.add_word('jklmnopq')
lp.add_word('r')
lp.add_word('stuv')
lp.end()
print()

rp = RightParagraph(8)
rp.add_word('abc')
rp.add_word('defg')
rp.add_word('hi')
rp.add_word('jklmnopq')
rp.add_word('r')
rp.add_word('stuv')
rp.end()

Вывод

abc defg
hi
jklmnopq
r stuv

abc defg
      hi
jklmnopq
  r stuv

Задача 3

Форматы записи дат в виде строки в США и Европе отличаются. В США принят формат мм.дд.гггг, в Европе — дд.мм.гггг, где дд — день (дополняется нулём слева, если число меньше 10), мм — месяц (так же дополняется нулём слева), гггг — год. Например, 10 апреля 2000 года будет записано в американском формате как 04.10.2000, а в европейском — как 10.04.2000. Все годы в задаче — четырёхзначные. Реализуйте классы AmericanDate и EuropeanDate. При инициализации они должны принимать год, месяц и число (именно в этом порядке). Так же должны быть реализованы методы set_year, set_month, set_day для изменения одной из компонентов даты, и get_year, get_month, get_day для чтения компонентов даты. Метод format должен возвращать строковое представление (своё для каждого класса). Гарантируется, что все даты в тестах корректны и существуют в календаре.

Тест

Ввод

# Ваш код

american = AmericanDate(2000, 4, 10)
european = EuropeanDate(2000, 4, 10)
print(american.format())
print(european.format())

Вывод

04.10.2000
10.04.2000

Задача 4

Реализуйте классы MinStat, MaxStat, AverageStat, которые будут находить минимум, максимум и среднее арифметическое последовательности целых чисел. Экземпляры классов инициализируются без аргументов. Метод add_number должен добавлять в статистику число, которое будет учтено при вычислении финального результата методом result. Для экземпляров MinStat и MaxStat result должен возвращать целое число, для AverageStat — число типа float (это число будет сравниваться с правильным ответом до седьмой значащей цифры). Если в последовательности отсутствуют числа и статистические величины вычислить невозможно, метод result должен возвращать None.

Тест 1

Ввод

# Ваш код

values = [1, 2, 4, 5]

mins = MinStat()
maxs = MaxStat()
average = AverageStat()
for v in values:
    mins.add_number(v)
    maxs.add_number(v)
    average.add_number(v)

print(mins.result(), maxs.result(), '{:<05.3}'.format(average.result()))

Вывод

1 5 3.000

Тест 2

Ввод

# Ваш код

mins = MinStat()
maxs = MaxStat()
average = AverageStat()

print(mins.result(), maxs.result(), average.result())

Вывод

None None None

Тест 3

Ввод

# Ваш код

values = [1, 0, 0]

mins = MinStat()
maxs = MaxStat()
average = AverageStat()
for v in values:
    mins.add_number(v)
    maxs.add_number(v)
    average.add_number(v)

print(mins.result(), maxs.result(), '{:<05.3}'.format(average.result()))

Вывод

0 1 0.333

Задача 5

Реализуйте класс Table, который хранит целые числа в двумерной таблице.

При инициализации Table(rows, cols) экземпляру передаются число строк и столбцов в таблице. Строки и столбцы нумеруются с нуля. Ячейки таблицы инициализируются нулями.

  • table.get_value(row, col) — прочитать значение из ячейки со строкой row, столбцом col. Если ячейка с индексами row и col не лежит внутри таблицы, нужно вернуть None.
  • table.set_value(row, col, value) — записать число в ячейку со строкой row, столбцом col. Гарантируется, что в тестах будет в запись только в ячейки внутри таблицы.
  • table.n_rows() — вернуть число строк в таблице.
  • table.n_cols() — вернуть число столбцов в таблице.

Тест 1

Ввод

# Ваш код

tab = Table(3, 5)
tab.set_value(0, 1, 10)
tab.set_value(1, 2, 20)
tab.set_value(2, 3, 30)
for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()

Вывод

0 10 0 0 0
0 0 20 0 0
0 0 0 30 0

Тест 2

Ввод

# Ваш код

tab = Table(2, 2)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.set_value(0, 0, 10)
tab.set_value(0, 1, 20)
tab.set_value(1, 0, 30)
tab.set_value(1, 1, 40)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

for i in range(-1, tab.n_rows() + 1):
    for j in range(-1, tab.n_cols() + 1):
        print(tab.get_value(i, j), end=' ')
    print()
print()

Вывод

0 0
0 0

10 20
30 40

None None None None
None 10 20 None
None 30 40 None
None None None None

Тест 3

Ввод

# Ваш код

tab = Table(1, 1)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.set_value(0, 0, 1000)

Вывод

0

1000

None None None
None 1000 None
None None None

Задача 6

Реализуйте класс Rectangle для описания прямоугольника, стороны которого параллельны осям координат.

  • При инициализации экземпляра передаются координаты левой нижней точки прямоугольника x и y, а также его ширина и высота w и h. Таким образом, координаты верхнего правого угла — (x + w) и (y + h).
  • При вызове метода intersection (например, rect1.intersection(rect2)) должен возвращаться прямоугольник, который возникает как пересечение rect1 и rect2. Если прямоугольники не пересекаются, должен возвращаться объект None.
  • Также необходимо реализовать метод get для каждого из атрибутов класса, возвращающий заданное значение данного атрибута. Пример: get_*() - возвращает значение атрибута *.

Гарантируется, что во входных данных ширина и высота любого прямоугольника положительны. Если пересечением прямоугольников является точка или отрезок, то следует считать, что они не пересекаются.

Тест 1

Ввод

# Ваш код

rect1 = Rectangle(0, 0, 10, 10)
rect2 = Rectangle(5, 5, 10, 10)
rect3 = rect1.intersection(rect2)

if rect3 is None:
    print('No intersection')
else:
    print(rect3.get_x(), rect3.get_y(), rect3.get_w(), rect3.get_h())

Вывод

5 5 5 5

Тест 2

Ввод

# Ваш код

rect1 = Rectangle(0, 0, 10, 10)
rect2 = Rectangle(10, 0, 10, 10)
rect3 = rect1.intersection(rect2)

if rect3 is None:
    print('No intersection')
else:
    print(rect3.get_x(), rect3.get_y(), rect3.get_w(), rect3.get_h())

Вывод

No intersection

Тест 3

Ввод

# Ваш код

rect1 = Rectangle(3, 5, 2, 1)
rect2 = Rectangle(1, 2, 10, 10)
rect3 = rect1.intersection(rect2)

if rect3 is None:
    print('No intersection')
else:
    print(rect3.get_x(), rect3.get_y(), rect3.get_w(), rect3.get_h())

Вывод

3 5 2 1

Задача 7

Реализуйте класс Table, который хранит целые числа в двумерной таблице.

  • При инициализации Table(rows, cols) экземпляру передаются число строк и столбцов в таблице. Строки и столбцы нумеруются с нуля.
  • table.get_value(row, col) — прочитать значение из ячейки в строке row, столбце col. Если ячейка с индексами row и col не лежит внутри таблицы, нужно вернуть None.
  • table.set_value(row, col, value) — записать число в ячейку строки row, столбца col. Гарантируется, что в тестах будет в запись только в ячейки внутри таблицы.
  • table.n_rows() — вернуть число строк в таблице
  • table.n_cols() — вернуть число столбцов в таблице
  • table.delete_row(row) — удалить строку с номером row
  • table.delete_col(col) — удалить колонку с номером col
  • table.add_row(row) — добавить в таблицу новую строку с индексом row. Номера строк >= row, должны увеличится на единицу. Новая строка состоит из нулей.
  • table.add_col(col) — добавить в таблицу новую колонку с индексом col. Номера колонок >= col, должны увеличится на единицу. Новая колонка состоит из нулей.

Тест 1

Ввод

# Ваш код

tab = Table(3, 5)
tab.set_value(0, 1, 10)
tab.set_value(1, 2, 20)
tab.set_value(2, 3, 30)
for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.add_row(1)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

Вывод

0 10 0 0 0
0 0 20 0 0
0 0 0 30 0

0 10 0 0 0
0 0 0 0 0
0 0 20 0 0
0 0 0 30 0

Тест 2

Ввод

# Ваш код

tab = Table(2, 2)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.set_value(0, 0, 10)
tab.set_value(0, 1, 20)
tab.set_value(1, 0, 30)
tab.set_value(1, 1, 40)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

for i in range(-1, tab.n_rows() + 1):
    for j in range(-1, tab.n_cols() + 1):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.add_row(0)
tab.add_col(1)

for i in range(-1, tab.n_rows() + 1):
    for j in range(-1, tab.n_cols() + 1):
        print(tab.get_value(i, j), end=' ')
    print()
print()

Вывод

0 0
0 0

10 20
30 40

None None None None
None 10 20 None
None 30 40 None
None None None None

None None None None None
None 0 0 0 None
None 10 0 20 None
None 30 0 40 None
None None None None None

Тест 3

Ввод

# Ваш код

tab = Table(1, 1)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.set_value(0, 0, 1000)

for i in range(tab.n_rows()):
    for j in range(tab.n_cols()):
        print(tab.get_value(i, j), end=' ')
    print()
print()

for i in range(-1, tab.n_rows() + 1):
    for j in range(-1, tab.n_cols() + 1):
        print(tab.get_value(i, j), end=' ')
    print()
print()

tab.add_row(0)
tab.add_row(2)
tab.add_col(0)
tab.add_col(2)

tab.set_value(0, 0, 2000)
tab.set_value(0, 2, 3000)
tab.set_value(2, 0, 4000)
tab.set_value(2, 2, 5000)

for i in range(-1, tab.n_rows() + 1):
    for j in range(-1, tab.n_cols() + 1):
        print(tab.get_value(i, j), end=' ')
    print()
print()

Вывод

0

1000

None None None
None 1000 None
None None None

None None None None None
None 2000 0 3000 None
None 0 1000 0 None
None 4000 0 5000 None
None None None None None

Задача 8

Для создания смс-рассылки напишите классы Person и Company, а также функцию send_sms. Объект класса Person при инициализации принимает значения имени, отчества и фамилии человека, а также словарь с номерами его телефонов. Класс должен содержать следующие методы: - get_phone() – возвращает телефон из словаря по ключу private или None, если такого телефона нет, - get_name() – возвращает фамилию, имя и отчество человека через пробел, - get_work_phone() – возвращает телефон из словаря по ключу work или None, если такого телефона нет, - get_sms_text() – возвращает текст Уважаемый <имя> <отчество>! Примите участие в нашем беспроигрышном конкурсе для физических лиц.

Объект класса Company при инициализации принимает название компании, тип компании, словарь с её телефонами, а также неограниченное количество работников компании (объектов класса Person). Класс должен содержать следующие методы:

  • get_phone() – возвращает телефон из словаря по ключу contact, если его нет, то телефон первого работника, у которого есть телефон по ключу work, или None, если таких работников не найдётся,
  • get_name() – возвращает название компании,
  • get_sms_text() – возвращает текст Для компании <название компании> есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для <тип компании>.

Функция send_sms должна принимать неограниченное количество объектов класса Person или Company и в случае, если найден номер для отправки (с помощью метода get_phone()), выводить сообщение Отправлено СМС на номер <номер> с текстом: <Текст СМС>, иначе – текст Не удалось отправить сообщение абоненту: <ФИО человека или название компании>.

Тест 1

Ввод

# Ваш код

person1 = Person("Ivan", "Ivanovich", "Ivanov", {"private": 123, "work": 456})
person2 = Person("Ivan", "Petrovich", "Petrov", {"private": 789})
person3 = Person("Ivan", "Petrovich", "Sidorov", {"work": 789})
person4 = Person("John", "Unknown", "Doe", {})
company1 = Company("Bell", "ООО", {"contact": 111}, person3, person4)
company2 = Company("Cell", "АО", {"non_contact": 222}, person2, person3)
company3 = Company("Dell", "Ltd", {"non_contact": 333}, person2, person4)
send_sms(person1, person2, person3, person4, company1, company2, company3)

Вывод

Отправлено СМС на номер 123 с текстом: Уважаемый Ivan Ivanovich! Примите участие в нашем беспроигрышном конкурсе для физических лиц
Отправлено СМС на номер 789 с текстом: Уважаемый Ivan Petrovich! Примите участие в нашем беспроигрышном конкурсе для физических лиц
Не удалось отправить сообщение абоненту: Sidorov Ivan Petrovich
Не удалось отправить сообщение абоненту: Doe John Unknown
Отправлено СМС на номер 111 с текстом: Для компании Bell есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для ООО
Отправлено СМС на номер 789 с текстом: Для компании Cell есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для АО
Не удалось отправить сообщение абоненту: Dell

Тест 2

Ввод

# Ваш код

person1 = Person("Степан", "Петрович", "Джобсов", {"private": 555})
person2 = Person("Боря", "Иванович", "Гейтсов", {"private": 777, "work": 888})
person3 = Person("Семен", "Робертович", "Возняцкий", {"work": 789})
person4 = Person("Леонид", "Арсенович", "Торвальдсон", {})
company1 = Company("Яблочный комбинат", "ООО", {"contact": 111}, person1, person3)
company2 = Company("ПластОкно", "АО", {"non_contact": 222}, person2)
company3 = Company("Пингвинья ферма", "Ltd", {"non_contact": 333}, person4)
send_sms(person1, person2, person3, person4, company1, company2, company3)

Вывод

Отправлено СМС на номер 555 с текстом: Уважаемый Степан Петрович! Примите участие в нашем беспроигрышном конкурсе для физических лиц
Отправлено СМС на номер 777 с текстом: Уважаемый Боря Иванович! Примите участие в нашем беспроигрышном конкурсе для физических лиц
Не удалось отправить сообщение абоненту: Возняцкий Семен Робертович
Не удалось отправить сообщение абоненту: Торвальдсон Леонид Арсенович
Отправлено СМС на номер 111 с текстом: Для компании Яблочный комбинат есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для ООО
Отправлено СМС на номер 888 с текстом: Для компании ПластОкно есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для АО
Не удалось отправить сообщение абоненту: Пингвинья ферма