2.22. Наследование¶
Наследование позволяет классу расширить другой класс – начать со всех его методов и атрибутов, а затем добавить или переопределить то, что отличается. Исходный класс называется базовым (или родительским); расширение называется подклассом.
Square повторно использует каждый метод Shape и переопределяет те, которые нужно специализировать.¶
2.22.1. Определение подкласса¶
Базовый класс указывается в скобках в строке class:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
Вывод:
square with area 16
Square наследует describe от Shape без изменений и переопределяет area, чтобы возвращать реальное значение. Базовый метод describe по-прежнему работает, потому что он вызывает self.area() – который во время выполнения разрешается в переопределение.
2.22.2. super()¶
super() возвращает прокси, позволяющий методу вызвать версию метода из базового класса. Наиболее частое применение – вызов базового __init__ из __init__ подкласса, чтобы базовый класс мог инициализировать свои собственные атрибуты:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
Забывание super().__init__ – распространённый источник ошибок: атрибуты, которые установил бы базовый класс, никогда не устанавливаются, и унаследованные методы, опирающиеся на них, позже падают.
2.22.2.1. Неявный базовый класс¶
Каждый класс неявно наследует от object, корня иерархии типов Python. Методы вроде __repr__, __eq__ и механизмы доступа к атрибутам – всё это приходит оттуда, даже когда класс не объявляет базового класса. Явная запись class Foo(object): и запись class Foo: эквивалентны в современном Python; последняя является общепринятой формой.
2.22.3. Когда наследование помогает – и когда нет¶
Используйте наследование, когда один класс действительно является более конкретной разновидностью другого, разделяя большую часть поведения. Классический тест – проверка «является»: Square является Shape.
Когда два класса просто случайно разделяют несколько вспомогательных функций, наследование избыточно. Прибегните вместо этого к композиции: пусть один класс хранит экземпляр другого в качестве атрибута и использует его через этот атрибут. Форма – «имеет», а не «является»:
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
Оба варианта работают. Композиционный вариант обычно лучше, потому что он:
Сохраняет интерфейсы небольшими.
WorkerComposesпредоставляет толькоrunиlogger.WorkerInheritsтакже предоставляетlog– вызывающие могут напрямую написатьworker.log(...), независимо от того, было ли это задумано.Разделяет жизненные циклы. Логгер можно заменить, разделить между рабочими объектами или создать по-разному для каждого, и всё это без изменения класса
Worker. Подкласс жёстко прикреплён к своему базовому классу.Избегает коллизий имён методов. Два базовых класса, оба определяющих
run, конфликтуют, когда класс пытается унаследовать от обоих; два атрибута никогда не конфликтуют.
Практическое эмпирическое правило:
Используйте наследование, когда подкласс действительно является разновидностью базового класса и каждый метод базового класса имеет для него смысл –
SquareявляетсяShape;MemoryErrorявляетсяException.Используйте композицию во всех остальных случаях – когда вам нужно лишь поведение другого класса, а не его идентичность. Большинство реального кода использует композицию гораздо чаще, чем наследование.