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

Гибкость объектно-ориентированного программирования проявляется в наследовании. Так называют возможность на основе существующих классов создавать классы-наследники, изменяя свойства и методы родительских классов, и добавляя новые.
Наследование организует иерархическую структуру проекта. Разработчик определяет классу-родителю основные свойства и методы, к которым можно обращаться в объектах любого из дочерних классов.
image
Синтаксис наследования в Python выглядит так:
Скопировать кодPYTHON
# у класса может быть несколько родительских классов class <Имя нового_класса>(<Имя класса-родителя 1>[, <Имя класса-родителя 2>, ...]): <тело класса>
Пример:
Скопировать кодPYTHON
class User: def __init__(self, name, phone): self.name = name self.phone = phone def show(self): print(f'{self.name} ({self.phone})') # объявляем класс Friend, дочерний по отношению к классу User class Friend(User): def show(self): print(f'Имя: {self.name} || Телефон: {self.phone}') # создаём объекты User и Friend father = User("Дюма-отец", "+33 3 23 96 23 30") son = Friend("Дюма-сын", "+33 3 23 96 23 30")
Класс User — родительский для класса Friend. Все свойства и методы родительского класса наследуются: в объектах класса Friend мы можем обращаться к свойствам name и phone, а также вызывать метод show().
Но в классе Friend метод show сработает иначе, чем в объекте User: этот метод был переопределён, описан заново, и данные на экран выведутся в другом формате.
Скопировать кодPYTHON
# вызываем метод show() класса User (родительского) father.show() # результат: # Дюма-отец (+33 3 23 96 23 30) # вызываем метод show() класса Friend (дочернего) son.show() # результат выглядит иначе, чем у объекта User: # Имя: Дюма-сын || Телефон: +33 3 23 96 23 30
Отношения между двумя этими классами принято описывать так:
Предположим, в объектах класса Friend вы хотите сохранять не только имя и телефон, но и адрес. Значит, при создании экземпляра класса Friend нужно передавать параметр address. Но конструктор родительского класса принимает только name и phone.
В этом случае поможет переопределение родительского конструктора, функции __init__.
Скопировать кодPYTHON
class User: def __init__(self, name, phone): self.name = name self.phone = phone def show(self): print(f'{self.name} ({self.phone})') # наследуем класс Friend от User class Friend(User): # пишем конструктор класса-наследника, чтобы он принимал все нужные параметры def __init__(self, name, phone, address): # наследуем функциональность конструктора из класса-родителя super().__init__(name, phone) # добавляем новую функциональность: свойство address self.address = address # полностью переопределяем родительский метод show() def show(self): print(f'Имя: {self.name} || Телефон: {self.phone} || Адрес: {self.address}')
При создании экземпляра класса Friend будет вызван конструктор класса __init__.
Первым делом в нём вызывается функция super(), в неё передаются значения name и phone. При этом происходит вызов конструктора родительского класса и его функциональность сохраняется в классе-наследнике.
Так можно вызвать не только конструктор, но и любой другой метод родительского класса, ведь конструктор — это тоже метод класса, хоть и немного особенный.
После вызова super() сохраняем значение address в свойство self.address.
Теперь мы можем одинаково обращаться к любому из трех полей, как к унаследованным, так и к добавленному. Например, в методе show() мы можем напечатать адрес друга.
Если в дочернем классе вы хотите сохранить функциональность класса-родителя, то в дочернем классе нужно вызвать функцию super(), как в примере с конструктором.
Если же вы хотите полностью изменить поведение конструктора класса или иного метода, то вызывать super() нет необходимости, в этом случае нужно полностью написать конструктор дочернего класса.
Именно это мы и сделали в примере, но не для конструктора, а для метода show(): мы перезаписали его полностью.

Важные термины

На собеседованиях или при чтении научной литературы потребуется знать основные термины теории ООП.
Интерфейс класса — это функциональная часть класса, через которую происходит взаимодействие с самим классом или с экземпляром этого класса.
Описывая класс, разработчик одновременно создает интерфейс для обращения к этому классу и к его экземплярам. Для класса User интерфейсом будут те части класса или объекта, с которыми может взаимодействовать другой код:
Скопировать кодPYTHON
class Bird: # Это конструктор, он вызывается при создании объекта def __init__(self, name, size): self.name = name self.size = size def show(self): # вызывается для вывода на экран всех свойств объекта # это интерфейс класса, к нему можно обратиться из внешнего кода print(f'{self.name} носит одежду размера «{self.size}».') # Создание объекта sparrow = Bird('Воробей', 'S') # Теперь можно воспользоваться его внешним интерфейсом: методом show() sparrow.show() # Результат: Воробей носит одежду размера «S».
Наследование — способ описать новый класс на базе существующего. При этом в дочернем классе можно сохранить или переопределить свойства и методы родительского класса.
Механизм наследования прозрачен: если функция или свойство родительского класса ещё раз описаны в дочернем классе, то этот метод или это свойство будут переопределены. А остальные свойства и методы будут работать точно так же, как у класса-родителя.
Скопировать кодPYTHON
class Bird: def __init__(self, name, size): # Это конструктор, он вызывается при создании объекта self.name = name self.size = size def show(self): # вызывается для вывода на экран всех свойств объекта print(f'{self.name} носит одежду размера «{self.size}».') class Parrot(Bird): def __init__(self, name, size, sound): super().__init__(name, size) self.sound = sound def show(self): # вызывается для вывода на экран всех свойств объекта print(f'{self.name} носит одежду размера «{self.size}» и {self.sound}.') # Создание объектов sparrow = Bird('Воробей', 'S') ara = Parrot('Попугай ара', 'XL', 'разговаривает') nymphicus = Parrot('Попугай Корелла', 'S', 'щебечет') # Теперь можно воспользоваться его внешним интерфейсом: методом show() sparrow.show() ara.show() nymphicus.show() # Результат: # Воробей носит одежду размера «S». # Попугай ара носит одежду размера «XL» и разговаривает. # Попугай Корелла носит одежду размера «S» и щебечет.
Инкапсуляция — объединение и скрытие методов и свойств, и предоставление доступа к ним через простой внешний интерфейс.
Даже не имея понятия, как работают методы lower, upper или split объекта типа str, мы из документации знаем о них и можем управлять объектом. Методы «инкапсулированы», а разработчику предоставлен интерфейс для их вызова: string.upper() А класс Parrot инкапсулирует свойства попугая name, size, sound, и его метод show(): совершенно необязательно знать, как они работают, можно просто обратиться к ним и получить результат: ara.show()
Полиморфизм — возможность взаимодействовать с объектами разных типов через одинаковые интерфейсы, обращаться к свойствам и методам, общим для всех объектов.
В примере с птичками от класса Bird наследуются классы Parrot и Predator, а от Predator наследуется класс Egg.
К какому бы наследнику класса Bird мы ни обратились через интерфейсы name или show() — мы получим ответ (или, как минимум, не получим ошибку), потому что мы предусмотрительно реализовали принцип полиморфизма: у всех наследников класса Bird есть эти интерфейсы.
Скопировать кодPYTHON
class Bird: def __init__(self, name, size): self.name = name self.size = size def show(self): print(f'{self.name} носит одежду размера «{self.size}».') class Parrot(Bird): def __init__(self, name, size, sound): super().__init__(name, size) self.sound = sound def show(self): print(f'{self.name} носит одежду размера «{self.size}» и {self.sound}.') class Predator(Bird): def __init__(self, name, size, claws_size): super().__init__(name, size) self.claws_size = claws_size def show(self): print(f'{self.name} носит одежду размера «{self.size}» и когти размера {self.claws_size}.') class Egg(Predator): def show(self): print(f'Из яйца вылупится птичка {self.name} размера «{self.size}» с когтями размера {self.claws_size}.')