Наследование
Contents
Наследование#
Наследование является ключевым аспектом ООП и, конечно, доступно в python
. Как уже многократно подмечалось ранее, в python
всё является объектом. Можно даже сказать больше, любой тип объекта (встроенный или пользовательский) или напрямую или через один из своих базовых классов расширяет тип object
.
В частности, следующее объявление класса неявно расширяет object
.
class MyClass:
pass
print(MyClass.__bases__)
issubclass(MyClass, object)
(<class 'object'>,)
True
Специальный атрибут __bases__ позволяет узнать базовый класс (или классы, python
поддерживает множественное наследование). Встроенная функция issubclass возвращает True
, отвечает на вопрос, является ли класс указанный в первом аргументе производным от класса указанного во втором аргументе.
Базовый синтаксис#
Допустим, у нас есть базовый класс BaseClass
и мы хотим объявить класс DerivedClass
, который будет расширять его. Тогда необходимо указать базовый класс в заголовочной строке производного класса в круглых скобках после имени базового класса.
class BaseClass:
pass
class DerivedClass(BaseClass):
pass
DerivedClass.__bases__
(__main__.BaseClass,)
В данном примере заголовок class DerivedClass(BaseClass):
сигнализирует, что DerivedClass
наследует от BaseClass
.
При проверке принадлежности экземпляра производного класса к базовому классу метод isinstance вернет True
.
d = DerivedClass()
print(isinstance(d, BaseClass))
print(type(d) == BaseClass)
True
False
Из-за этой особенности, принято проверять принадлежность к классу именно методом isinstance(obj, cls)
, а не выражением вида type(obj) == cls
. Это позволяет писать код, который не будет замечать разницы между экземплярами базового и производного классов. В ряде ситуаций область применения такого кода можно будет расширять, не редактируя его.
Наследование атрибутов класса#
Кроме того, производный класс наследует атрибуты и методы базового класса (речь пока идет про атрибуты самого класса, а не экземпляра).
Определим базовый класс с атрибутом attr
и со статическим (для удобства вызова) методом method
.
class BaseClass:
attr = "Атрибут класса BaseClass"
@staticmethod
def method():
print("Метод класса BaseClass")
class DerivedClass(BaseClass):
pass
DerivedClass.method()
print(DerivedClass.attr)
Метод класса BaseClass
Атрибут класса BaseClass
Видим, что через объект объявления производного класса DerivedClass
удаётся получить доступ к атрибутам и методам базового класса BaseClass
.
Под капотом.
Механизм наследования атрибутов не совсем очевиден. У объекта объявления производного класса не появляются атрибуты базового класса, но получить доступ к атрибуту базового класса через объект объявления производного класса получается из-за механизма поиска атрибутов класса.
Пусть мы пытаемся выражением C.x
получить доступ к атрибуту x
класса C
, который расширяет класс B
. Тогда выполняется следующая процедура.
Атрибут
x
ищется у классаC
. Если он обнаруживается, то он и возвращается;Если атрибут
x
у классаC
найти не удаётся, то атрибутx
ищется у базового классаB
.
Второй шаг выполняется рекурсивно, т.е. если B
расширяет класс A
и в B
тоже не удаётся найти атрибут x
, то поиск продолжится в классе A
(и далее по цепочке наследования).
Доступ к атрибутам базового класса через экземпляр производного класса тоже возможен, т.к. процедура поиска атрибута у экземпляра делегирует этот поиск классу этого экземпляра (см. процедуру выше), если в самом экземпляре нет такого атрибута.
Перекрытие атрибутов#
Если в производном классе есть атрибуты с такими же именами, как и в базовом классе, то они переопределят таковые из базового класса.
Расширим определение производного класса DerivedClass
из предыдущего примера его собственными атрибутами attr
и method
.
class BaseClass:
attr = "Атрибут класса BaseClass"
@staticmethod
def method():
print("Метод класса BaseClass")
class DerivedClass(BaseClass):
attr = "Атрибут класса DerivedClass"
@staticmethod
def method():
print("Метод класса DerivedClass")
DerivedClass.method()
print(DerivedClass.attr)
Метод класса DerivedClass
Атрибут класса DerivedClass
Видим, что теперь через объект объявления производного класса вызываются его же методы.
Множественное наследование#
Можно наследовать сразу от нескольких базовых классов. Для этого необходимо указать их через запятую.
class LeftBase:
a = "a left"
b = "b left"
class MiddleBase:
b = "b middle"
c = "c middle"
class RightBase:
c = "c right"
d = "d right"
class DerivedClass(LeftBase, MiddleBase, RightBase):
pass
В примере DerivedClass
наследует сразу от трех классов.
Атрибуты в базовых классах пересекаются: атрибут b
есть и у LeftBase
и у MiddleBase
, атрибут c
есть и у MiddleBase
и у RightBase
. Возникает вопрос, если обратиться от производного класса к этим атрибутам, то значение атрибута какого из базовых классов вернется в качестве результата? Распечатаем атрибуты a
, b
, c
и d
класса DerivedClass
.
for attr in "abcd":
print(f"{attr}: {getattr(DerivedClass, attr)}")
a: a left
b: b left
c: c middle
d: d right
Видим, что возвращается атрибут того базового класса, который указан в списке базовых классов первым (самый левый).
Вызов методов базового класса. Функция super
#
Иногда все же возникает необходимость вызвать перекрытый метод базового класса в экземпляре производного класса. Яркий пример — инициализация объекта. При создании объекта необходимо убедиться, что будет вызван и метод __init__
базового класса и метод __init__
производного класса. Обычно этого добиваются вызовом инициализирующего метода базового класса в самом начале инициализирующего метода производного класса.
Неопытному программисту на python
может показаться, что этого можно добиться следующим образом:
class A:
def __init__(self, x):
self.x = x
class B(A):
def __init__(self, x, y):
self.__init__(x)
self.y = y
Но это приведет к рекурсии: метод B.__init__
создаётся на этапе объявления класса, а значит при поиске атрибута self.__init__
найдется именно B.__init__
(у экземпляра self
такого атрибута нет, а значит поиск идёт в его классе), а не A.__init__
.
Выход из этой ситуации — вызвать метод A.__init__
явно.
class A:
def __init__(self, x):
self.x = x
class B(A):
def __init__(self, x, y):
A.__init__(self, x)
self.y = y
При таком подходе произойдет то, чего мы и добивались. Тем не менее принято делать это иначе, а именно использовать встроенную функцию super. В нашем примере, инструкция
A.__init__(self, x)
заменяется на
super().__init__(x)
Note
Параметр self
при вызове через super
передавать не надо!
class A:
def __init__(self, x):
print("Инициализация в A")
self.x = x
class B(A):
def __init__(self, x, y):
print("Инициализация в B")
super().__init__(x)
self.y = y
b = B(42, 3.14)
print(b.x, b.y)
Инициализация в B
Инициализация в A
42 3.14
В таком крошечном примере может показаться, что использование такого подхода с функцией super
ни чем не упрощает вызов методов базового класса. Тем не менее принято предпочитать именно его даже в самых простых ситуациях. В более сложных иерархиях классов без функции super
сложно обойтись.
Рассмотрим следующую иерархию наследования: South
наследует от West
и East
, каждый из которых в свою очередь расширяют класс North
. Хочется, чтобы при инициализации экземпляра класса South
вызывались и методы инициализации всех базовых классов. Попробуем реализовать эту схему, указывая все базовые классы явно.
class North:
def __init__(self):
print("North")
class West(North):
def __init__(self):
print("West")
North.__init__(self)
class East(North):
def __init__(self):
print("East")
North.__init__(self)
class South(West, East):
def __init__(self):
print("South")
West.__init__(self)
East.__init__(self)
s = South()
South
West
North
East
North
Видим, что метод инициализации класса North
вызвался дважды. Первый раз это произошло через класс West
, а второй раз через класс East
.
Теперь заменим все явные упоминания классов через функцию super
.
class North:
def __init__(self):
print("Up")
class West(North):
def __init__(self):
print("West")
super().__init__()
class East(North):
def __init__(self):
print("East")
super().__init__()
class South(West, East):
def __init__(self):
print("South")
super().__init__()
s = South()
South
West
East
Up
Проблема с тем, что метод инициализации North
вызывался дважды, решена! Функция super
использует С3-линеаризацию (method resolution order) для определения порядка, в котором вызывать методы классов в иерархии наследования. Но чтобы это работало, необходимо, чтобы везде вызов происходил именно через super
.
Абстрактный базовый класс. Абстрактный метод.#
Модуль abc (сокращение от Abstract Base Class
) предоставляет инструменты для реализации абстрактных базовых классов, т.е. классов, которые лишь задают интерфейс и не предназначены для создания экземпляров напрямую. Обычно, абстрактный базовый класс наследует от abc.ABC, а абстрактные методы помечаются декоратором abc.abstractmethod. Производные от такого абстрактного базового класса классы смогут создавать экземпляры, только если они переопределят все абстрактные методы. Если не переопределен хоть один из абстрактных методов, то python
возбудит ошибку при попытке создать экземпляр. Так как тело абстрактной функции не играет никакой роли, то в нем часто возбуждают исключение NotImplementedError.
В качестве примера реализуем абстрактный базовый класс Shape
для геометрической фигуры. Как и в примере с треугольником, будем считать, что каждая фигура должна уметь считать свой периметр и площадь.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def perimeter(self):
raise NotImplementedError
@abstractmethod
def area(self):
raise NotImplementedError
try:
Shape()
except TypeError as msg:
print(msg)
Can't instantiate abstract class Shape with abstract methods area, perimeter
Видим, что экземпляр класса Shape
создать не удаётся — у него есть абстрактные методы.
from math import pi
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def perimeter(self):
return 2 * pi * self.radius
try:
Circle()
except TypeError as msg:
print(msg)
Can't instantiate abstract class Circle with abstract methods area
Переопределение лишь одного метода не меняет ситуацию. Из сообщения ошибки можно понять, какие методы мы забыли доопределить.
from math import pi
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def perimeter(self):
return 2 * pi * self.radius
def area(self):
return pi * self.radius * self.radius
c = Circle(1)
print(c.area(), c.perimeter())
3.141592653589793 6.283185307179586