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

Подобно тому, как объекты наследуют характеристики классов, дочерние классы наследуют атрибуты от своих родительских классов. Они также могут переопределять существующие атрибуты и добавлять новые.

Простое наследование методов родительского класса

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

Связь между родительским и дочерним классом устанавливается через дочерний класс: имена родительских классов записываются в скобках после имени дочернего.

class Table:
    def __init__(self, l, w, h):
        self.lenght = l
        self.width = w
        self.height = h

class KitchenTable(Table):
    def setPlaces(self, p):
        self.places = p

class DeskTable(Table):
    def square(self):
        return self.width * self.length

Классы KitchenTable и DeskTable не определяют свои собственные конструкторы и наследуют его от родительского класса. Создавая их экземпляры, обязательно передавать аргументы в __init__(), иначе возникнет ошибка:

>>> from test import *
>>> t1 = KitchenTable()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 3 required positional arguments: 'l', 'w', and 'h'
>>> t1 = KitchenTable(2, 2, 0.7)
>>> t2 = DeskTable(1.5, 0.8, 0.75)
>>> t3 = KitchenTable(1, 1.2, 0.8)

Можно создавать объекты прямо из родительского класса Table. Однако им не будет доступен функционал setPlaces() и square() классам KitchenTable или DeskTable. Подобно тому, как экземпляр KitchenTable не может обращаться к методам и атрибутам DeskTable.

>>> t4 = Table(1, 1, 0.5)
>>> t2.square()
1.2000000000000002
>>> t4.square()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Table' object has no attribute 'square'
>>> t3.square()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'KitchenTable' object has no attribute 'square'

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

Полное переопределение метода надкласса

Если метод надкласса нам не подходит, мы можем ввести новый класс, являющийся подклассом DeskTable. Например, компьютерные столы, площадь рабочей поверхности которых нужно корректировать на определенную величину. В этом случае логично добавить собственный метод square() в подкласс:

class ComputerTable(DeskTable):
    def square(self, e):
        return self.width * self.length - e

При создании ComputerTable необходимо указывать параметры, поскольку интерпретатор будет искать конструктор у родителя и надкласса, и обнаружит метод __init__() именно там.

Когда вызывается square(), используется метод, реализованный в конкретном подклассе ComputerTable, оставляя метод square() из DeskTable невидимым и, соответственно, переопределенным.

>>> from test import ComputerTable
>>> ct = ComputerTable(2, 1, 1)
>>> ct.square(0.3)
1.7

Дополнение или расширение методов

Часто возникают ситуации, когда метод подкласса дублирует часть кода надкласса. Чтобы избежать этого, можно вызвать метод родительского класса и затем добавить свои элементы:

class ComputerTable(DeskTable):
    def square(self, e):
        return DeskTable.square(self) - e 

В данном случае вызывается метод из другого класса, затем он расширяется собственными выражениями, такими, как вычитание.

Рассмотрим другой пример. Если в KitchenTable не требуется метод, и поле places должно устанавливаться при создании объекта через конструктор, можно создать собственный конструктор с чистого листа, тем самым переопределяя родительский:

class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        self.length = l
        self.width = w
        self.height = h
        self.places = p

Однако такой подход неэффективен, если дублируется почти весь конструктор надкласса. Лучше вызвать исходный конструктор, а затем добавить дополнительные переменные:

class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        Table.__init__(self, l, w, h)
        self.places = p

Теперь при создании экземпляра KitchenTable необходимо передать в конструктор четыре аргумента. Первые три из них будут переданы по цепочке наследования, а последняя будет использована непосредственно здесь.

>>> tk = KitchenTable(2, 1.5, 0.7, 10)
>>> tk.places
10
>>> tk.width
1.5

Практическая работа

Самостоятельно ознакомьтесь с функцией super() и перепишите методы дочерних классов, расширяющие родительские, с использованием функции super(), где это возможно.

Вопросы для самопроверки:

  1. Что такое наследование в контексте объектно-ориентированного программирования?
  2. Чем отличаются классы от подклассов в ООП?
  3. Как устанавливается связь между родительским и дочерним классом в Python?
  4. Как метод подкласса может переопределять метод надкласса?
  5. В чем преимущество вызова метода родительского класса в подклассе вместо полного переопределения?
  6. Как дополнить метод родительского класса в подклассе?