Наследование
Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.
Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.
Простое наследование методов родительского класса
В качестве примера рассмотрим два класса столов. Класс Table – родительский по отношению к DeskTable (письменные столы). Независимо от своего типа все столы имеют длину, ширину и высоту. Пусть для письменных столов также важна площадь поверхности. Общее вынесем в класс, частное – в подкласс.
Наследственная связь между классами устанавливается через подкласс. При определении дочернего после его имени в скобках указывается родительский.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length t1 = Table(1.5, 1.8, 0.75) t2 = DeskTable(0.8, 0.6, 0.7) print(t2.square()) # вывод: 0.48
В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.
С другой стороны, экземпляры надкласса Table, согласно неким родственным связям, не наследуют метод square своего подкласса.
В этом смысле терминология "родительский и дочерний класс" не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.
Полное переопределение метода надкласса
Рассмотрим вариант программы с "цепочкой наследования". Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length class ComputerTable(DeskTable): def square(self, monitor=0.0): return self.width * self.length - monitor t3 = ComputerTable(0.8, 0.6, 0.7) print(t3.square(0.3)) # вывод: 0.18
Допустим, по задумке разработчиков рабочая поверхность компьютерного стола может вычисляться за вычетом площади, которую занимает монитор. В результате метод square в ComputerTable имеет отличия.
Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable.
В то же время ComputerTable наследует конструктор класса от своей "бабушки" – класса Table.
Дополнение, оно же расширение, метода
Часто требуется не столько заменить, то есть полностью переопределить, метод родительского класса в дочернем, сколько дополнить, то есть расширить, код метода родительского класса в дочернем. В таких случаях решением является вызов метода надкласса в теле соответствующего метода подкласса. Обычно после этого в теле метода подкласса пишется дополнительный код.
Пусть имеется подкласс кухонных столов, для которых необходимо еще одно поле – количество посадочных мест. Из-за этой детали мы вынуждены в дочернем классе переопределить конструктор родительского.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): self.length = l self.width = w self.height = h self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Поскольку существенная часть кода конструктора подкласса является такой же как в надклассе, правильнее будет вызвать метод другого класса, а не дублировать код:
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): Table.__init__(self, l, w, h) self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Здесь в теле конструктора KitchenTable мы вызываем метод __init__
через объект-класс Table, а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы "вручную" передаем текущий экземпляр (self), записывая его перед остальными аргументами.
У кода выше есть небольшой недостаток. Нам ничего не мешает (при условии совпадения количества параметров) вызвать конструктор другого класса, а не только родительского, указав его имя вместо Table. Кроме того, имя надкласса может измениться, и тогда есть риск неправильных обращений к нему из дочерних классов.
В Python с целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super
. Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:
class KitchenTable(Table): def __init__(self, l, w, h, p): super().__init__(l, w, h) self.places = p
В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.
Параметры со значениями по умолчанию у родительского класса
Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, p, l, w, h): Table.__init__(self, l, w, h) self.places = p
При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:
t = Table()
Можем ли мы создать экземпляр от KitchenTable, передав значение только для параметра p? Например, вот так:
k = KitchenTable(10)
Возможно ли, что p будет присвоено число 10, а l, w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:
... k = KitchenTable(10) TypeError: __init__() missing 3 required positional arguments: 'l', 'w', and 'h'
Когда создается объект от дочернего класса, сначала вызывается его конструктор, если он есть. Интерпретатор еще не знает, что в теле этого конструктора будет вызван конструктор родительского класса. Ведь это не обязательно. Значит, если все параметры дочернего конструктора не имеют значений по умолчанию, при построении объекта все значения должны передаваться.
Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l=1, w=1, h=0.7, p=4): Table.__init__(self, l, w, h) self.places = p
Параметр p, которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые "интерфейсы", то есть одинаковое количество передаваемых в конструктор аргументов.
Поэтому лучше, когда методы родственных классов принимают одинаковое число параметров. А если разное, то у "лишних" должны быть значения по-умолчанию, чтобы при вызове конструктора их можно было бы не передавать. Если такие параметры находятся еще и в конце, передачу аргументов для предстоящих параметров можно выполнять без ключей.
Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): places = 4 def set_places(self, p): self.places = p
Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places, можем вызвать метод set_places. Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places.
k = KitchenTable() k.places = 6
Поэтому метод set_places в общем-то не нужен.
В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h if isinstance(self, KitchenTable): p = int(input("Сколько мест: ")) self.places = p
С помощью функции isinstance
проверяется, что создаваемый объект имеет тип KitchenTable. Если это так, то у него появляется поле places.
Мы не используем параметр p со значением по умолчанию в заголовке конструктора потому, что, если объектам других родственных классов он не нужен, не происходило бы путаницы и сложностей с документированием кода.
Практическая работа
Разработайте программу по следующему описанию.
В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод "иду за героем", который в качестве аргумента принимает объект типа "герой". У героев есть метод увеличения собственного уровня.
В основной ветке программы создается по одному герою для каждой команды. В цикле генерируются объекты-солдаты. Их принадлежность команде определяется случайно. Солдаты разных команд добавляются в разные списки.
Измеряется длина списков солдат противоборствующих команд и выводится на экран. У героя, принадлежащего команде с более длинным списком, увеличивается уровень.
Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.
Курс с примерами решений практических работ:
pdf-версия