Наследование в ООП
Гибкость объектно-ориентированного программирования проявляется в наследовании. Так называют возможность на основе существующих классов создавать классы-наследники, изменяя свойства и методы родительских классов, и добавляя новые.
Наследование организует иерархическую структуру проекта. Разработчик определяет классу-родителю основные свойства и методы, к которым можно обращаться в объектах любого из дочерних классов.
Синтаксис наследования в 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})')
class Friend(User):
def show(self):
print(f'Имя: {self.name} || Телефон: {self.phone}')
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
father.show()
son.show()
Отношения между двумя этими классами принято описывать так:
- User является родительским классом для Friend;
- Friend является дочерним классом для User, или Friend наследуется от User.
Предположим, в объектах класса 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})')
class Friend(User):
def __init__(self, name, phone, address):
super().__init__(name, phone)
self.address = address
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')
sparrow.show()
Наследование — способ описать новый класс на базе существующего. При этом в дочернем классе можно сохранить или переопределить свойства и методы родительского класса.
Механизм наследования прозрачен: если функция или свойство родительского класса ещё раз описаны в дочернем классе, то этот метод или это свойство будут переопределены. А остальные свойства и методы будут работать точно так же, как у класса-родителя.
Скопировать код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', 'щебечет')
sparrow.show()
ara.show()
nymphicus.show()
Инкапсуляция — объединение и скрытие методов и свойств, и предоставление доступа к ним через простой внешний интерфейс.
Даже не имея понятия, как работают методы 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}.')