Объектно-ориентированное программирование. Полиморфизм и наследование

Полиморфизм

В этой главе мы рассмотрим очень мощные инструменты для создания больших и сложных программ: полиморфизм и наследование. Они позволят нам создавать объекты, которые будут иметь различную реализацию, но одинаковый интерфейс. Если задуматься, мы уже неоднократно встречали в Python3 такие объекты: например, строки, списки, кортежи с словари поддерживают вычисление числа элементов в помощью функции len, извлечение элемента по индексу или ключу с помощью синтаксиса a[b].

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

Однако, для полиморфизма необходима некоторая база. В Python3 эта база строится на предоставлении объектами одинакового интерфейса методов. Например, функция str(obj), которая преобразует некоторый объект obj в строку, на самом деле просто возвращает результат выполнения obj.__str__() для этого объекта. То есть, определяя в разных классах разные методы __str__, мы задаём разные строковые представления для разных классов объектов.

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

class Cat:
    def __str__(self):
        return '<Cat meow>'


class Dog:
    def __str__(self):
        return '<Dog woof>'


c = Cat()
d = Dog()
print(123, 'hello', c, d)

Данный код выведет на печать строку

123 hello <Cat meow> <Dog woof>

Как видно, сработал полиморфизм метода __str__.

Более сложный пример на полиморфизм

Давайте определим два класса, Cat и Dog, которые будут реализовывать кошку и собаку и предоставлять интерфейс для различных действий, которые эти животные могут выполнять. Например, для бега и сна. Эти операции будут иметь одинаковый интерфейс, то есть, одинаково называться и иметь одни и те же аргументы. Это позволит использовать их, не зная, к какому именно классу (Cat или Dog) принадлежит объект, к которому мы эти операции применяем. Также у класса Dog будет операция “принеси мяч”, у которой нет аналога в классе Cat.

class Cat:
    def __init__(self, size, color):
            self.size = size
            self.color = color

    def run(self):
        print('Кошка носится по квартире как сумасшедшая')

    def sleep(self, hours):
        print('Спит {} часов'.format(hours))


class Dog:
    def __init__(self, size, color, mood):
        self.size = size
        self.color = color
        self.mood = mood

    def run(self):
        print('Собака цивильно бегает по парку')

    def sleep(self, hours):
        print('Спит {} часов'.format(hours))

    def get_ball(self):
        if self.mood == 'good':
            print('Собака несёт мяч')
        else:
            print('Собака лежит на коврике')

Теперь создадим по объекту обоих классов и вызовем доступные для них методы.

c = Cat(10, 'Red')
d = Dog(15, 'Grey', 'good')

c.run()     # Кошка носится по квартире как сумасшедшая
c.sleep(5)  # Спит 5 часов

d.run()       # Собака цивильно бегает по парку
d.sleep(7)    # Спит 7 часов
d.get_ball()  # Собака несёт мяч

Более того, как сказано выше, мы специально спроектировали методы run и sleep так, чтобы их можно было вызывать, не заботясь о том, применяем мы их к объекту класса Cat или класса Dog. То есть эти методы являются полиморфными и на их основе можно написать полиморфную функцию:

def run_and_sleep(pet, sleep_hours):
    pet.run()
    pet.sleep(sleep_hours)

run_and_sleep(c, 5)  # Кошка носится по квартире как сумасшедшая
                     # Спит 5 часов
run_and_sleep(d, 7)  # Собака цивильно бегает по парку
                     # Спит 7 часов

Обратите внимание, что run_and_sleep работает как с кошками и собаками, так и с объектами других классов, которые мы, возможно, добавим в программу позже и которые будут поддерживать тот же “протокол взаимодействия” в виде методов run и sleep. В том, что мы можем писать код для ещё не созданных классов, заключается сила полиморфизма.

Наследование

Наследование позволяет унаследовать часть поведения от родительского класса. В примере ниже общее поведение вынесено в класс Animal, поведение которого затем наследуют классы Cat и Dog.

class Animal:
    def __init__(self, size, color):
        self.size = size
        self.color = color

    def sleep(self, n_hours):
        print('Сплю {} часов'.format(n_hours))

class Cat(Animal):
    def run(self):
            print('Кошка носится по квартире как сумасшедшая')

class Dog(Animal):
    def __init__(self, size, color, mood):
        super().__init__(size, color)
        self.mood = mood

    def run(self):
        print('Собака цивильно бегает по парку')

    def get_ball(self):
        if self.mood == 'good':
            print('Собака несёт мяч')
        else:
            print('Собака лежит на коврике')

Использовать эти классы можно точно так же, как и классы Cat и Dog из предыдущего примера.

_images/inheritance.png

На рисунке в левой рамке показана иерархия наследования: классы Cat и Dog наследуют класс Animal, класс Animal, которому мы не указали родительского класса, по умолчанию наследует класс object. В нём содержатся некоторые полезные стандартные методы и благодаря тому, что он является классом-родителем для классов, у которых родитель не указан явно, он является корнем любой иерархии наследования.

В двух средних рамках на рисунке показаны объекты c и d, которые принадлежат классам Cat и Dog соответственно, на правых рамках – строки кода, работающие с этими объектами. Линиями на этой картинке обозначены пути поиска свойств и методов. Для того чтобы понять, что происходит, давайте разберём правила поиска свойств и методов.

При поиске свойств, если мы запишем obj.x или self.x свойство x будет найдено в самом объекте. Так, например, в объектах класса Cat хранятся свойства size и color, а в объектах класса Dogsize, color и mood. (Примечание: существуют свойства, которые хранятся в классах, но мы их в нашем курсе не рассматриваем.)

При поиске же методов, например obj.do_something() или self.do_something() всё сложнее. В объектах методов нет, интерпретатор их не находит и продолжает поиск в классе, создавшем этот объект. Если метод найден, он будет вызван, если нет, поиск продолжится в классе-родителе данного класса, потом в классе-родителе родителя и так далее. Если интерпретатор не найдёт метод с данным именем, произойдёт ошибка AttributeError.

Также, знакомая нам проверка типа isinstance(obj, cls) проверяет принадлежность объекта не только классу cls, но и любому из его потомков. То есть, верно и выражение isinstance(c, Animal), и isinstance(d, Animal). В большинстве случаев такое поведение именно то, что вам нужно. Если же требуется точное соответствие без классов-потомков, используйте выражение type(obj) == cls.

Несколько советов

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

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

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

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