MidТеория7 min

Наследование и полиморфизм

super(), MRO, миксины, abc.ABC, @abstractmethod, @override и Protocol

Наследование позволяет создавать новые классы на основе существующих, переиспользуя код и расширяя функциональность. Python поддерживает множественное наследование, абстрактные классы и протоколы.

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

class Animal:
    """Base class for all animals."""

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def speak(self) -> str:
        return "..."

    def info(self) -> str:
        return f"{self.name}, {self.age} лет"

class Dog(Animal):
    """Dog inherits from Animal."""

    def __init__(self, name: str, age: int, breed: str) -> None:
        super().__init__(name, age)  # call parent __init__
        self.breed = breed

    def speak(self) -> str:  # override parent method
        return "Гав!"

    def fetch(self, item: str) -> str:  # new method
        return f"{self.name} принёс {item}"

class Cat(Animal):
    def speak(self) -> str:
        return "Мяу!"

# Usage
dog = Dog("Бобик", 3, "Овчарка")
cat = Cat("Мурка", 5)

print(dog.speak())    # Гав!
print(cat.speak())    # Мяу!
print(dog.info())     # Бобик, 3 лет (inherited)
print(dog.fetch("палку"))  # Бобик принёс палку

# isinstance and issubclass
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True
print(isinstance(cat, Dog))     # False
print(issubclass(Dog, Animal))  # True

super() -- вызов родительского метода

class Shape:
    def __init__(self, color: str = "чёрный") -> None:
        self.color = color

    def describe(self) -> str:
        return f"Фигура цвета {self.color}"

class Circle(Shape):
    def __init__(self, radius: float, color: str = "красный") -> None:
        super().__init__(color)  # call Shape.__init__
        self.radius = radius

    def describe(self) -> str:
        base = super().describe()  # call Shape.describe
        return f"{base}, круг с радиусом {self.radius}"

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

circle = Circle(5.0)
print(circle.describe())
# Фигура цвета красный, круг с радиусом 5.0
print(f"Площадь: {circle.area():.2f}")
# Площадь: 78.54

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

class A:
    def method(self) -> str:
        return "A"

class B(A):
    def method(self) -> str:
        return "B"

class C(A):
    def method(self) -> str:
        return "C"

class D(B, C):
    pass

d = D()
print(d.method())  # 'B' (first in MRO after D)

# Method Resolution Order (MRO) - C3 linearization
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

# MRO determines the order super() follows
class A:
    def method(self) -> str:
        return "A"

class B(A):
    def method(self) -> str:
        return f"B -> {super().method()}"

class C(A):
    def method(self) -> str:
        return f"C -> {super().method()}"

class D(B, C):
    def method(self) -> str:
        return f"D -> {super().method()}"

d = D()
print(d.method())  # D -> B -> C -> A
# super() follows MRO, not just the direct parent!

Миксины (Mixins)

import json
from datetime import datetime

class JsonMixin:
    """Mixin that adds JSON serialization capability."""

    def to_json(self) -> str:
        return json.dumps(self.__dict__, default=str, ensure_ascii=False)

    @classmethod
    def from_json(cls, json_str: str):
        data = json.loads(json_str)
        return cls(**data)

class TimestampMixin:
    """Mixin that adds timestamp tracking."""

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()

    def touch(self) -> None:
        """Update the modification timestamp."""
        self.updated_at = datetime.now()

class LogMixin:
    """Mixin that adds logging capability."""

    def log(self, message: str) -> None:
        class_name = type(self).__name__
        print(f"[{class_name}] {message}")

# Combine mixins with a base class
class User(JsonMixin, LogMixin):
    def __init__(self, name: str, email: str) -> None:
        self.name = name
        self.email = email

    def __repr__(self) -> str:
        return f"User({self.name!r}, {self.email!r})"

user = User("Иван", "[email protected]")
print(user.to_json())
# {"name": "Иван", "email": "[email protected]"}

user.log("Пользователь создан")
# [User] Пользователь создан

Абстрактные классы (ABC)

from abc import ABC, abstractmethod
import math

class Shape(ABC):
    """Abstract base class for geometric shapes."""

    @abstractmethod
    def area(self) -> float:
        """Calculate the area of the shape."""
        ...

    @abstractmethod
    def perimeter(self) -> float:
        """Calculate the perimeter of the shape."""
        ...

    def describe(self) -> str:
        """Non-abstract method (inherited by all subclasses)."""
        return (
            f"{type(self).__name__}: "
            f"площадь={self.area():.2f}, периметр={self.perimeter():.2f}"
        )

# Cannot instantiate abstract class
# shape = Shape()  # TypeError: Can't instantiate abstract class

class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

# Usage with polymorphism
shapes: list[Shape] = [
    Circle(5),
    Rectangle(3, 4),
    Circle(10),
]

for shape in shapes:
    print(shape.describe())
# Circle: площадь=78.54, периметр=31.42
# Rectangle: площадь=12.00, периметр=14.00
# Circle: площадь=314.16, периметр=62.83

# Total area (polymorphism in action)
total = sum(s.area() for s in shapes)
print(f"Общая площадь: {total:.2f}")

Абстрактные свойства и методы класса

from abc import ABC, abstractmethod

class Database(ABC):
    """Abstract database interface."""

    @property
    @abstractmethod
    def connection_string(self) -> str:
        """Return the database connection string."""
        ...

    @abstractmethod
    def connect(self) -> None:
        ...

    @abstractmethod
    def execute(self, query: str) -> list[dict]:
        ...

    @classmethod
    @abstractmethod
    def from_config(cls, config: dict) -> "Database":
        ...

class PostgreSQL(Database):
    def __init__(self, host: str, port: int, dbname: str) -> None:
        self.host = host
        self.port = port
        self.dbname = dbname

    @property
    def connection_string(self) -> str:
        return f"postgresql://{self.host}:{self.port}/{self.dbname}"

    def connect(self) -> None:
        print(f"Подключение к {self.connection_string}")

    def execute(self, query: str) -> list[dict]:
        print(f"Выполнение: {query}")
        return []

    @classmethod
    def from_config(cls, config: dict) -> "PostgreSQL":
        return cls(config["host"], config["port"], config["dbname"])

@override (Python 3.12+)

from typing import override

class Base:
    def process(self) -> str:
        return "base"

    def validate(self) -> bool:
        return True

class Child(Base):
    @override
    def process(self) -> str:  # OK: overrides Base.process
        return "child"

    # @override
    # def procsess(self) -> str:  # Error at type-check: typo in method name!
    #     return "child"

Полиморфизм

# Duck typing - "If it walks like a duck..."
class FileLogger:
    def write(self, message: str) -> None:
        with open("app.log", "a") as f:
            f.write(message + "\n")

class ConsoleLogger:
    def write(self, message: str) -> None:
        print(f"[LOG] {message}")

class DatabaseLogger:
    def write(self, message: str) -> None:
        print(f"[DB] Saving: {message}")

# All work with the same interface - no inheritance needed!
def log_event(logger, event: str) -> None:
    """Works with any object that has a write() method."""
    logger.write(f"Event: {event}")

log_event(ConsoleLogger(), "User logged in")
log_event(DatabaseLogger(), "Order created")

# Polymorphism with built-in types
def total_length(items: list) -> int:
    """Works with anything that has __len__."""
    return sum(len(item) for item in items)

print(total_length(["hello", "world"]))      # 10
print(total_length([[1, 2], [3, 4, 5]]))     # 5
print(total_length([{1, 2, 3}, {4, 5}]))     # 5

Protocol (Structural Typing)

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    """Any object with a draw() method."""
    def draw(self) -> str: ...

@runtime_checkable
class Resizable(Protocol):
    """Any object with a resize() method."""
    def resize(self, factor: float) -> None: ...

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def draw(self) -> str:
        return f"Circle(r={self.radius})"

    def resize(self, factor: float) -> None:
        self.radius *= factor

class Text:
    def __init__(self, content: str) -> None:
        self.content = content

    def draw(self) -> str:
        return f"Text: {self.content}"

# Protocol check at runtime
circle = Circle(5)
text = Text("Привет")

print(isinstance(circle, Drawable))   # True
print(isinstance(text, Drawable))     # True
print(isinstance(circle, Resizable))  # True
print(isinstance(text, Resizable))    # False

# Use in type hints
def render(items: list[Drawable]) -> None:
    for item in items:
        print(item.draw())

render([circle, text])
# Circle(r=5)
# Text: Привет

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

# PREFER composition over inheritance

# BAD: deep inheritance hierarchy
class Animal: ...
class Mammal(Animal): ...
class Pet(Mammal): ...
class Dog(Pet): ...
class GuideDog(Dog): ...  # too deep!

# GOOD: composition with delegation
class Engine:
    def __init__(self, horsepower: int) -> None:
        self.horsepower = horsepower

    def start(self) -> str:
        return f"Двигатель {self.horsepower}HP запущен"

class GPS:
    def navigate(self, destination: str) -> str:
        return f"Маршрут до {destination} построен"

class Car:
    """Car composed of Engine and GPS (HAS-A, not IS-A)."""

    def __init__(self, model: str, engine: Engine, gps: GPS | None = None) -> None:
        self.model = model
        self.engine = engine  # composition
        self.gps = gps        # optional component

    def start(self) -> str:
        return f"{self.model}: {self.engine.start()}"

    def navigate(self, destination: str) -> str:
        if self.gps is None:
            return "GPS не установлен"
        return self.gps.navigate(destination)

car = Car("Tesla", Engine(300), GPS())
print(car.start())              # Tesla: Двигатель 300HP запущен
print(car.navigate("Москва"))   # Маршрут до Москва построен

Итоги

  • super() вызывает методы родителя, следуя MRO
  • MRO (C3-линеаризация) определяет порядок поиска при множественном наследовании
  • Миксины -- небольшие классы для добавления функциональности
  • ABC + @abstractmethod -- интерфейсы через наследование
  • Protocol -- структурная типизация (duck typing) без наследования
  • @override (Python 3.12+) -- проверка переопределения методов
  • Предпочитайте композицию наследованию (HAS-A вместо IS-A)
  • Наследование до 2-3 уровней; глубже -- используйте композицию

Проверь себя

Зачем вызывать super().__init__() в дочернем классе?

Что такое MRO (Method Resolution Order)?

Что произойдёт при попытке создать экземпляр абстрактного класса?

Чем Protocol отличается от ABC для определения интерфейсов?