python
February 9, 2023

Как создавать абстрактные классы в Python и когда они пригодятся

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

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

class ScraperBase:
    
    def walk(self):
        while(1):
            url = self.get_new_url()
            if not url:
                break
            self.parse_items_in_url(url)
            
    def get_new_url(self):
        raise NotImplementedError()
        
    def parse_items_in_url(self, url):
        raise NotImplementedError()
        

Как и говорил выше, в основном классе мы можем создать скелет обхода страниц, а детали отдаются на откуп конкретной реализации (для гарантии этого выбрасываем исключение), так как для разных сайтов они будут меняться (например, для Авито, Юлы, Циан). Реализуем игрушечный шаблон подкласса, чтобы метод get_new_url возвращал 1 при атрибуте класса менее трех, а потом останавливал наш цикл обхода в walk (посылая в качестве очередного url 0):

class AvitoScraper(ScraperBase):
    timer = 0
    def get_new_url(self):
        self.__class__.timer+=1
        if self.__class__.timer<3:
            print(self.__class__.timer)
            return 1
        else:
            self.__class__.timer = 0
            print('exiting')
            return 0
        
scr = AvitoScraper()
scr.walk()

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

def f(self, url):
    print('parsing items')
AvitoScraper.parse_items_in_url = f

scr.walk()
scr.walk()

Указанная методика создания макета для базового класса является рабочей, однако уступает более современному способу, который извещает о необходимости реализации методов не на этапе исполнения, а создания экземпляра подкласса.

Для этого базовый шаблон следует сделать подклассом ABCMeta модуля abc, а требуемые для реализации методы помечать как абстрактные декоратором @abstractmethod. Кроме того, экземпляр такого класса в отличие от примера выше (ScraperBase) создать нельзя, что оправданно, так как он не обладает всем необходимым функционалом:

from abc import ABCMeta, abstractmethod

class ScraperBase(metaclass=ABCMeta):  
    def walk(self):
        while(1):
            url = self.get_new_url()
            if not url:
                break
            self.parse_items_in_url(url)

    @abstractmethod        
    def get_new_url(self):
        pass
    
    @abstractmethod    
    def parse_items_in_url(self, url):
        pass

bc = ScraperBase()
class AvitoScraper(ScraperBase):
    timer = 0
    def get_new_url(self):
        self.__class__.timer+=1
        if self.__class__.timer<3:
            print(self.__class__.timer)
            return 1
        else:
            self.__class__.timer = 0
            print('exiting')
            return 0

scr = AvitoScraper()

Без объявления всех методов экземпляр не создается.

class AvitoScraper(ScraperBase):
    
    timer = 0
    def get_new_url(self):
        self.__class__.timer+=1
        if self.__class__.timer<3:
            print(self.__class__.timer)
            return 1
        else:
            self.__class__.timer = 0
            print('exiting')
            return 0
        
    def parse_items_in_url(self, url):
        print('parsing items')
        
scr = AvitoScraper()

scr.walk()