Модуль abc (Abstract Base Classes)

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Стандартная библиотека Python содержит весьма полезный модуль abc (Abstract Base Classes), с помощью которого очень удобно описывать абстрактные методы. Об абстрактных методах мы с вами уже говорили в курсе по ООП языка Python. Но для целостности восприятия, повторим их еще раз.

Рассмотрим классический пример. Пусть у нас имеется несколько классов геометрических фигур: Line, Rect, Triangle. Каждый из них имеет метод с одним и тем же именем draw.

Тогда, имея набор из объектов этих классов:

figures = [Line(), Rect(), Triangle(), Line(), Rect()]

мы с помощью единого интерфейса (метода) draw() можем отобразить их на экране:

for f in figures:
    f.draw()

Но при такой реализации классов сразу возникает один тонкий момент. Программист должен знать и помнить, что в классах геометрических фигур обязательно должен присутствовать метод draw() для ее рисования. Эта проблема особенно актуальна при разработке больших проектов, когда зачастую программа реализуется группой разработчиков, и все должны согласованно взаимодействовать друг с другом. В этих условиях очень просто прописать вместо метода draw() какой-то другой, или вовсе забыть его реализовать внутри класса. Один из способов решения этой проблемы, унаследовать все классы геометрических фигур от некоторого базового класса, в котором будет определен метод draw(), как абстрактный метод. Особенность абстрактного метода в том, что его необходимо переопределять в дочерних классах, иначе возникнет ошибка при создании объекта класса. Соответственно, программист сразу заметит, что было сделано неверно, и сможет оперативно подправить код программы.

Язык Python так устроен, что в нем нельзя объявлять по настоящему абстрактные методы, как это делается в некоторых других ЯП, например, в C++ или Java. Но можно имитировать абстракцию. Ранее мы с вами делали это следующим образом. Создавали базовый класс, например, с именем Geom, и в нем метод draw():

class Geom:
    def draw(self):
        raise NotImplementedError("В дочернем классе должен быть переопределен метод draw(self)")

Тогда, наследуя дочерние классы фигур от базового класса Geom:

class Line(Geom):
    def draw(self):
        print("Line")
 
 
class Rect(Geom):
    def draw(self):
        print("Rect")
 
 
class Triangle(Geom):
    def draw(self):
        print("Triangle")

мы вынуждены для корректной их работы переопределять метод draw(). Иначе, если этого не сделать, то при обращении к этому методу:

figures = [Line(), Rect(), Triangle(), Line(), Rect()]
for f in figures:
    f.draw()

возникнет исключение (ошибка) с сообщением:

NotImplementedError: В дочернем классе должен быть переопределен метод draw(self)

Но эту ошибку мы увидим не на этапе создания объектов, а на этапе вызова метода draw() для соответствующего объекта. Такую ошибку можно не сразу отследить, а в более сложных случаях даже пропустить. Поэтому гораздо лучшим вариантом будет использование модуля abc для определения абстрактных методов.

Вначале следует импортировать этот модуль, например, командой:

import abc

В этом модуле имеется класс-обертка ABC, который позволяет имитировать объявление абстрактных методов с помощью декоратора abstractmethod:

class Geom(abc.ABC):
    @abc.abstractmethod
    def draw(self): 
        pass

Теперь, если в каком-либо дочернем классе не переопределять абстрактный метод draw(), например, в классе Line:

class Line(Geom):
    pass
#    def draw_line(self):
#        print("Line")

то при создании объекта этого класса возникнет ошибка:

TypeError: Can't instantiate abstract class Line with abstract method draw

Также нельзя создавать отдельно и объекты класса Geom, так как он содержит абстрактный метод:

g = Geom() # TypeError: Can't instantiate abstract class Geom with abstract method draw

Если бы метод draw() не был абстрактным, тогда никаких ошибок не возникало бы:

class Geom(abc.ABC):
    def draw(self):
        pass
 
g = Geom() # ok

Спрашивается, зачем тогда наследоваться от класса ABC? Дело в том, что декоратор abstractmethod и другие подобные работают только внутри классов, унаследованных от ABC. Если это наследование убрать:

class Geom:
    @abc.abstractmethod
    def draw(self):
        pass

то никакого эффекта от декоратора abstractmethod не будет. Он работает только в сочетании с базовым классом ABC.

Конечно, класс Geom может содержать не только абстрактные методы, но и любые другие конструкции, допустимые внутри классов, в том числе и обычные переменные и методы. Например:

class Geom(abc.ABC):
    def __init__(self, color=0, width=1):
        self.color = color
        self.width = width
 
    @abc.abstractmethod
    def draw(self):
        pass
 
    def get_data(self):
        return self.color, self.width

Имеем один абстрактный метод draw(), обычный метод get_data() и стандартный инициализатор __init__().

Абстрактными можно также делать и методы уровня класса и статические методы. Для этого следует последовательно прописать по два декоратора, например:

class Geom(abc.ABC):
    def __init__(self, color=0, width=1):
        self.color = color
        self.width = width
 
    @abc.abstractmethod
    def draw(self):
        pass
 
    @classmethod
    @abc.abstractmethod
    def get_limits(cls):
        pass
 
    @staticmethod
    @abc.abstractmethod
    def get_info():
        pass
 
    def get_data(self):
        return self.color, self.width

Соответственно, в дочерних классах все абстрактные методы должны быть переопределены. Например:

class Line(Geom):
    MIN_COORD = 0
    MAX_COORD = 100
 
    def draw(self):
        print("Line")
 
    @classmethod
    def get_limits(cls):
        return cls.MIN_COORD, cls.MAX_COORD
 
    @staticmethod
    def get_info():
        return "Class Line"

Использование абстрактных базовых классов весьма полезно при разработке больших проектов, где важно обеспечить согласованность структуры классов и интерфейсов. Это помогает избегать ситуаций, когда один разработчик ожидает определённый набор методов, а другой забывает реализовать какой-то важный элемент функциональности.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме