April 13, 2024

ABC или Абстрактные классы в Python

Работая с сложными объектами, которые имеют похожее назначение, часто можно попасться на удочку дублирования. Решить подобную проблему, предоставить общий интерфейс классам и пришел модуль abc или Abstract Base Classes.

Кроме того, это отличный способ практиковать хорошие принципы проектирования и использования парадигм ООП.

Итак, абстрактный класс - это класс, в котором реализован хотя бы 1 абстрактный метод. Можно представить его как чертеж, по которому будут строиться дочерние классы. В свою очередь, абстрактный метод - это метод-заглушка, то есть без реализации и нужен он затем, чтобы показать, что этот метод обязательно должен быть реализован в всех дочерних классах.

Кроме того, в абстрактном классе могут присутствовать не только абстрактные методы, но и методы с конкретной реализацией, которые можно просто брать и использовать в дочерних классах.

Интересной особенностью абстрактного класса является то, что нельзя создать его экземпляр, только от дочерних классов. Кроме того, создать экземпляр дочернего класса не получится, если в нем не реализованы все абстрактные методы абстрактного базового класса.

Такие дела. А теперь пойдем смотреть, как это работает на практике.

Представим, что нам надо сделать общий интерфейс с какими-либо "методами" музыкальных инструментов. Ведь все они могут звучать, и должны быть отстроены перед игрой, только делается это по-разному. Попробуем реализовать это с помощью абстрактных классов:

from abc import ABC, abstractmethod

class MusicInstruments(ABC):
    @abstractmethod
    def make_sound(self):
        raise NotImplementedError

    @abstractmethod
    def tune(self):
        raise NotImplementedError

Абстрактный класс обязательно должен быть унаследован от abc.ABC.

Я обычно явно прописываю вызов исключений в абстрактных методах вместо ... или pass, чтобы ошибочные попытки обратиться к реализации в абстрактном классе увенчались провалом. Выглядит это так:

class Guitar(MusicInstruments):
    def make_sound(self):
        ...

    def tune(self):
        super().tune()
    
    
gibson = Guitar()
gibson.tune()
> NotImplementedError

А что произойдет, если мы забудем реализовать все абстрактный методы в дочернем классе?

Ну во-первых, IDE подсветит этот момент:

А во-вторых, при попытке создать экземпляр такого класса, будет вызвано иключение TypeError:

class Guitar(MusicInstruments):
    def make_sound(self):
        ...

gibson = Guitar()

>    gibson = Guitar()
>             ^^^^^^^^
> TypeError: Can't instantiate abstract class Guitar with abstract method tune

А теперь рассмотрим успешный пример реализации концепции базовых классов:

from abc import ABC, abstractmethod


class MusicInstruments(ABC):
    @abstractmethod
    def make_sound(self):
        raise NotImplementedError

    @abstractmethod
    def tune(self):
        raise NotImplementedError


class Guitar(MusicInstruments):
    def make_sound(self):
        print("Брынь")

    def tune(self):
        print("Все струны отстроены, давай играть!")


class Drums(MusicInstruments):
    def make_sound(self):
        print("Бдыщ!")

    def tune(self):
        print("Пластики подтянуты и отстроены, можно играть!")


guitar = Guitar()
drums = Drums()

guitar.tune()
drums.tune()

guitar.make_sound()
drums.make_sound()

> Все струны отстроены, давай играть!
> Пластики подтянуты и отстроены, можно играть!
> Брынь
> Бдыщ!

Ну вот, другое дело! Теперь наши классы построены по образу и подобию абстрактного класса MusicInstruments и мы стали на несколько шагов ближе к написанию качественного кода.