Наследование, super(), MRO и множественное наследование

Python поддерживает наследование - один класс получает атрибуты и методы другого. Также поддерживается множественное наследование (один класс может иметь несколько родителей), что не во всех языках есть. Это даёт силу через mixins, но создаёт сложность - решает её алгоритм C3 для определения порядка методов (MRO).

Базовое наследование

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Some sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

d = Dog("Rex")
print(d.name)      # Rex - унаследован от Animal.__init__
print(d.speak())   # Woof! - переопределён

Dog(Animal) - Dog наследует от Animal. Атрибуты и методы родителя доступны в подклассе. Переопределение метода затмевает родительский.

super() - вызов метода родителя

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes some sound"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # вызов Animal.__init__
        self.breed = breed

    def speak(self):
        base = super().speak()    # вызов Animal.speak
        return f"{base} (it's a bark)"

d = Dog("Rex", "Labrador")
print(d.speak())   # Rex makes some sound (it's a bark)

super() это «proxy» на родительский класс. Не зашитое имя - super() работает корректно при изменении иерархии.

В Python 2 нужно было super(Dog, self).__init__(name). С Python 3 - просто super().

Когда init нужно явно вызывать super

class Base:
    def __init__(self):
        self.shared = []

class Child(Base):
    def __init__(self):
        super().__init__()   # ОБЯЗАТЕЛЬНО - иначе self.shared не появится
        self.extra = []

c = Child()
print(c.shared)   # [] - работает благодаря super
print(c.extra)    # []

Без super().__init__() атрибуты, инициализируемые в Base.init, не появятся на инстансе Child. Это типичная ошибка - забыть super.

С @dataclass super вызывать не нужно - он генерируется автоматически.

Множественное наследование

class JsonMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class TimestampMixin:
    def get_timestamp(self):
        import time
        return time.time()

class User(JsonMixin, TimestampMixin):
    def __init__(self, name):
        self.name = name

u = User("Alice")
print(u.to_json())         # {"name": "Alice"}
print(u.get_timestamp())   # 1735315200.0

User наследует от двух mixin-ов. Каждый добавляет свои методы. Это паттерн mixin - небольшие классы с одной фичей, комбинируемые в составной класс.

MRO - Method Resolution Order

Когда вызываешь obj.method(), Python ищет method в:

  1. Самом классе obj
  2. Родителях в порядке MRO

MRO рассчитывается алгоритмом C3 linearization:

class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):
    pass

d = D()
print(d.method())   # B
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

MRO для D: D → B → C → A → object. Метод ищется в этом порядке - найдено в B первым, возвращает "B".

C3 гарантирует:

  • Дочерние раньше родителей
  • Если A раньше B в одном классе, A раньше B везде
  • Стабильность относительно изменений

Diamond inheritance

class A:
    def method(self):
        print("A.method")

class B(A):
    def method(self):
        print("B.method")
        super().method()

class C(A):
    def method(self):
        print("C.method")
        super().method()

class D(B, C):
    def method(self):
        print("D.method")
        super().method()

D().method()
# D.method
# B.method
# C.method
# A.method

Каждый super() идёт по MRO, не к «реальному родителю». Это позволяет diamond inheritance работать корректно - A.method() вызывается ровно один раз. Без C3 был бы дублированный вызов.

super() в C вызывает A, потому что C идёт перед A в MRO для D. В одиночном наследовании это просто родитель, но при multiple - это следующий в MRO.

Распространённое заблуждение: super() это «вызов родителя». На самом деле - вызов следующего метода в MRO, что в одиночном наследовании совпадает с родителем, но в множественном может быть совсем другим классом.

Это позволяет mixins корректно «передавать управление дальше» через цепочку - паттерн cooperative multiple inheritance.

Проверка иерархии

isinstance(obj, ClassName)        # obj это инстанс ClassName или подкласса
issubclass(SubClass, ParentClass) # SubClass подкласс ParentClass

isinstance(Dog("Rex"), Animal)    # True
isinstance(Dog("Rex"), Dog)       # True
issubclass(Dog, Animal)           # True
issubclass(Dog, object)           # True - всё наследует object

isinstance и issubclass поддерживают tuple для проверки нескольких типов:

isinstance(x, (int, float))       # int OR float

Объект object - корень иерархии

Все классы в Python неявно наследуют от object:

class A:
    pass

print(A.__mro__)   # (<class 'A'>, <class 'object'>)

object даёт базовые dunder-методы: __init__, __repr__, __eq__, __hash__, __str__. Их можно переопределять.

В Python 2 нужно было явно class A(object): для «new-style» классов. В Python 3 это происходит автоматически.

Mixin pattern

Mixin - класс, добавляющий одну небольшую фичу. Не предназначен для самостоятельного использования:

class TimestampMixin:
    """Добавляет created_at и updated_at."""

    def __init__(self):
        from datetime import datetime
        self.created_at = datetime.now()
        self.updated_at = self.created_at

    def touch(self):
        from datetime import datetime
        self.updated_at = datetime.now()


class ReprMixin:
    """Автоматический __repr__ из __dict__."""

    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{self.__class__.__name__}({attrs})"


class User(TimestampMixin, ReprMixin):
    def __init__(self, name):
        super().__init__()   # вызовет TimestampMixin.__init__
        self.name = name


u = User("Alice")
print(u)         # User(created_at=..., updated_at=..., name='Alice')
u.touch()
print(u)         # updated_at изменился

Mixins должны быть орthogonal - не зависеть друг от друга и от конкретных классов.

Композиция vs наследование

Современная практика: prefer composition over inheritance:

# Наследование - жёсткое
class Car(Engine, Wheels, Body):   # хрупко, MRO сложный
    pass

# Композиция - гибкое
class Car:
    def __init__(self, engine, wheels, body):
        self.engine = engine
        self.wheels = wheels
        self.body = body

    def start(self):
        self.engine.start()

Композиция:

  • Гибче: легко заменить компоненты
  • Тестируемее: можно подменять mock-объектами
  • Понятнее: нет MRO-магии

Наследование оправдано для is-a отношения (Dog is an Animal), композиция - для has-a (Car has an Engine).

init_subclass - hook на создание подкласса

class Plugin:
    registry = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry[cls.__name__] = cls

class AuthPlugin(Plugin):
    pass

class LoggingPlugin(Plugin):
    pass

print(Plugin.registry)
# {'AuthPlugin': <class 'AuthPlugin'>, 'LoggingPlugin': <class 'LoggingPlugin'>}

__init_subclass__ вызывается при создании каждого подкласса. Удобно для регистрации плагинов, валидации структуры, других meta-операций без метаклассов.

Abstract methods через ABC

Если хочешь обязать подклассы реализовать метод:

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def get(self, key):
        ...

    @abstractmethod
    def set(self, key, value):
        ...

class MemoryStorage(Storage):
    def __init__(self):
        self.data = {}

    def get(self, key):
        return self.data.get(key)

    def set(self, key, value):
        self.data[key] = value

# Storage()   # TypeError - cannot instantiate abstract class
m = MemoryStorage()

Подробно про ABC и Protocol - в уроке про абстрактные классы и протоколы.

Распространённые ошибки

1. Забыли super().init()

class Base:
    def __init__(self):
        self.shared = []

class Bad(Base):
    def __init__(self):
        # super().__init__() пропущен
        self.extra = []

b = Bad()
print(b.shared)   # AttributeError - не инициализирован

2. super() с аргументами в Python 3

# Старо (Python 2 / некоторые legacy)
super(Dog, self).__init__(name)

# Современно (Python 3)
super().__init__(name)

С Python 3 super() без аргументов - короче и менее error-prone.

3. Перепутали порядок родителей в multiple inheritance

class A:
    def method(self):
        return "A"

class B:
    def method(self):
        return "B"

class C(A, B): ...
class D(B, A): ...

C().method()   # "A" - A первый в MRO
D().method()   # "B" - B первый

Порядок имеет значение - первый родитель приоритетнее в lookup.

Сравнение с Go и PHP

В Go нет наследования - используется композиция через embedding структур, а полиморфизм даёт интерфейс:

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes sound"
}

type Dog struct {
    Animal       // embedding - не наследование, но похоже
    Breed string
}

d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
d.Speak()   // "Rex makes sound" - через embedding

В PHP - одиночное наследование классов, но множественное через traits:

trait JsonExport {
    public function toJson(): string {
        return json_encode(get_object_vars($this));
    }
}

class User {
    use JsonExport;
    public function __construct(public string $name) {}
}

Python даёт самый гибкий механизм с multiple inheritance и MRO. Это сила и одновременно опасность - усложняет иерархии и отладку.

Мини-задание

  1. Базовое наследование с super:
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def describe(self):
        return f"{self.brand} {self.model}"

class Car(Vehicle):
    def __init__(self, brand, model, doors):
        super().__init__(brand, model)
        self.doors = doors

    def describe(self):
        return f"{super().describe()} ({self.doors} doors)"

c = Car("Toyota", "Camry", 4)
print(c.describe())   # Toyota Camry (4 doors)
  1. Mixin pattern:
class SerializableMixin:
    def to_dict(self):
        return self.__dict__.copy()

class ComparableMixin:
    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return self.__dict__ == other.__dict__

class Point(SerializableMixin, ComparableMixin):
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1.to_dict())     # {'x': 1, 'y': 2}
print(p1 == p2)         # True
  1. MRO:
class A:
    def hello(self):
        print("A")

class B(A):
    def hello(self):
        print("B")
        super().hello()

class C(A):
    def hello(self):
        print("C")
        super().hello()

class D(B, C):
    def hello(self):
        print("D")
        super().hello()

D().hello()
# D
# B
# C
# A

print(D.__mro__)
# (D, B, C, A, object)

Что дальше

Освоили наследование и MRO. В следующем уроке - dataclass и Enum: декларативное создание data-классов с автогенерацией методов и типы для перечислений.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.