Наследование, 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 в:
- Самом классе obj
- Родителях в порядке 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.
Это позволяет 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. Это сила и одновременно опасность - усложняет иерархии и отладку.
Мини-задание
- Базовое наследование с 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)
- 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
- 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-классов с автогенерацией методов и типы для перечислений.