Полиморфизм
После предыдущего занятия мы уже немного разбираемся в объектно-ориентированном программировании: освоили определение классов и методов, добавление атрибутов в объекты. Понятно, что классы, объекты, методы, атрибуты достаточно удобны и красивы, но в чем их преимущество перед функциями? Ведь некоторые объекты можно было бы передавать в функции и выполнять над ними те же действия, что и с помощью методов. Зачем Вводить дополнительный синтаксис и правила?
Основное преимущество в том, что объектно-ориентированный подход позволяет писать код, который будет работать с экземплярами различных классов. Иногда код может даже работать с классами, которые еще не созданы.
Свойство кода работать с разными типами данных называют полиморфизмом. Мы уже неоднократно пользовались этим свойством многих функций и операторов, не задумываясь о нем. Например, оператор + является полиморфным:
Внутренняя реализация оператора + существенно отличается для целых чисел, чисел с плавающей точкой и строк. То есть на самом деле это три разные операции — интерпретатор 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)))
Вывод
Тест 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)))
Вывод
Тест 3
Ввод
# Ваш код
values = []
selector = Selector(values)
odds = selector.get_odds()
evens = selector.get_evens()
print(' '.join(map(str, odds)))
print(' '.join(map(str, evens)))
Вывод
Задача 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()
Вывод
Задача 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())
Вывод
Задача 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()))
Вывод
Тест 2
Ввод
# Ваш код
mins = MinStat()
maxs = MaxStat()
average = AverageStat()
print(mins.result(), maxs.result(), average.result())
Вывод
Тест 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()))
Вывод
Задача 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()
Вывод
Тест 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()
Вывод
Тест 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)
Вывод
Задача 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())
Вывод
Тест 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())
Вывод
Тест 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())
Вывод
Задача 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)— удалить строку с номеромrowtable.delete_col(col)— удалить колонку с номеромcoltable.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()
Вывод
Тест 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 с текстом: Для компании ПластОкно есть супер предложение! Примите участие в нашем беспроигрышном конкурсе для АО
Не удалось отправить сообщение абоненту: Пингвинья ферма