2.22. Спадкування

Спадкування дозволяє класу розширювати інший клас – починати з усіма його методами та атрибутами, а потім додавати або перевизначати те, що відрізняється. Оригінальний клас називається базовим (або батьківським); розширення називається підкласом.

A base class Shape pointing down to a subclass Square; Square overrides one method (area) and inherits the rest.

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(...) безпосередньо, незалежно від того, чи це було задумано.

  • Розв’язує терміни існування. Logger можна замінити, поділити між workers або побудувати по-різному для кожного worker, не торкаючись класу Worker. Підклас нерозривно прив’язаний до свого базового.

  • Уникає зіткнень імен методів. Два базові класи, що обидва визначають run, конфліктують, коли клас намагається успадкувати від обох; два атрибути ніколи не конфліктують.

Практичне правило:

  • Використовуйте спадкування, коли підклас справді є різновидом базового і кожен метод базового має сенс у ньому – Square є Shape; MemoryError є Exception.

  • Використовуйте композицію для всього іншого – коли вам потрібна лише поведінка іншого класу, а не його ідентичність. У більшості реального коду композиція використовується значно частіше, ніж спадкування.