KirillHelm
@KirillHelm

Создание C++-like интерфейса в Python3?

Привет, начал знакомиться с питоном, пишу небольшой проект на нем.
Компоненты ПО должны быть написаны и сохранены каждый в своей директории. В которой в свою очередь должны быть две сабдиректории:
  • api
  • src

Думаю принцип понятен, в api должен быть 1 или несколько .py файлов, что должны предоставвлять api-class в виде:
class IInterface(object):
   def method(): pass

Подход я выбрал следующий. Создать базовый класс интерфейса:
class InterfaceBase(object):
   @staticmethod
   def create(*args): raise NotImplementedError

И что бы создать интерфейс нужно просто отнаследовать его и заинклюдить имплементацию:
from interface import InterfaceBase
from implementation import CImplementation
class IInterface(InterfaceBase):
   def create(*args): return CImplementation()
   def method(self): raise NotImplementedError

Ну и создаем имплементацию:
class IInterface(): pass
class CImplementation(IInterface):
   def method(self): print("CClass::method()")

Тут то и появилась проблема (:
Как я понял в питоне нет forward-declaration, следовательно у меня моя имплементация наследует не класс интерфейса, где описаны все методы, а непосредственно:
class IInterface(): pass
Казалось бы и так то все работает, но меня интересует, что бы выпадали NotImplementedError в случаи не реализации методов.
Так же хочу заметить, что заимпортить интерфейс в реализацию нельзя, по тому, что появляется циклическая зависемость.

Каким образом можно отнаследовать класс интерфейса - имплементацией?
  • Вопрос задан
  • 277 просмотров
Решения вопроса 1
KirillHelm
@KirillHelm Автор вопроса
Нашел следующее решение.
Интерфейс
Я создал базовый класс интерфейса InterfaceBase который наследует ABC, для реализации абстрактного класса интерфейса. Так же создал декоратор для класса, который решает следующие задачи:
  1. Наследует базовый класс интерфейса InterfaceBase
  2. Добавляет статический метод create(*args, **kwargs) этому классу именно с помощью этого можно будет создавать имплементацию напрямую из интерфейса (параметры переданные в функцию будут переданы в конструктор класса имплементации при его создании).

Так что теперь декларация интерфейса выглядит следующим образом:
@myinterfacedecorator
class IMyInterface():
   @abstractmethod
   def methodname(self, parameter1, paramer2): pass


Имплементация
Теперь перейдем к вкуснятинке - имплементации. Дело в том, что меня интересует два фактора:
  • Оторвать имплементацию полностью от интерфейса, что бы не аффектить ABC классом и его мета-классом на неё
  • Дать возможность подключения разных интерфейсов к одной и той же имплементации

Для этих целей я для начала сделал следующую цепочку:
  1. class InterfaceImplementationWrapper(object): pass - этот класс является базовым для класса имплементации, в нем я создаю дубликаты абстрактных методов с базовой имплементацией по типу:
    raise NotImplementedError("Method <name> is not implemented!")

  2. А так же класс, что наследует интерфейс и имеет аттрибут с имплементацией абстрактных методов интерфейса внутри себя. Имплементация же абстрактных методов есть суть wrapper-функций, что редеректят все вызовы в инстанс класса имплементации, что является полем в proxy (см. выше).
    class InterfaceImplementationProxy(IMyInterface): pass


Стоит заметить, что это все я генерирую динамически и оборачиваю в функцию implementInterface.
Таким образом мы получаем следующий код для декларации класса-имплементации:
class CMyInterfaceImplementation(implementInterface(IMyInterface)): pass

Но данная архитектура порождает проблему отсутствия механизма контролирующего создание инстанса класса имплементации, если абстрактные методы не перегружены (инстанс создать можно, только пользоваться не перегруженными методами не получиться -> приведет к exeption). По этому в proxy-класс был добавлен механизм в конструкторе, что проверяет все ли абстрактные методы интерфейса были имплементированы в классе реализации.

Подитожим:
Мы имеем интерфейс отнаследованный от базового класса интерфейсов (объявленный через декоратор @myinterfacedecorator), что в свою очередь является наследником ABC, каждый метод интерфейса объявлен через декоратор @abstractmethod.
Так же у нас есть имплементация, что отнаследована от wrapper-класса, который имеет методы такие же как в классе интерфейсе (но уже не абстрактные), с базовой имплементацией, которая вызывает exception.

Фабрика
Теперь перейдем к механизму, что создает имплементацию - фабрике. Мне показалось удобным создавать методы через статический метод интерфейса create, как упомянул выше. Именно он обращается к фабрике и запрашивает создание нового инстанса имплементации. Вопрос в том, как запросить класс имплементации у фабрики и как класс имплементации попадет в фабрику.
Фабрика представляет из себя singleton что хранит словарь, где каждому интерфейсу соответствует тип имплементации. Что бы зарегестрировать там новый интерфейс, следует просто вызвать необходимый метод, куда передается полный пусть в проекте к фуйлу интерфейса, имплементации, а так же имена соответствующих классов. Эта функция реализована на функционале библеотеки импортов. Эту функцию проще всего разместить в файле декларации интерфейса, так что она теперь выглядит так:
@myinterfacedecorator
class IMyInterface():
   @abstractmethod
   def methodname(self, parameter1, paramer2): pass
registerInterfaceAndImplementation("project.component.api.interface", "IMyInterface", "project.component.src.implementation", "CMyInterfaceImplementation")


Каждый раз когда пользователь хочет создать интерфейс, ему нужно только заимпортить api-класс (класс интерфейс) и вызвать его статический метод для создания:
from project.component.api.interface import IMyInterface
component = IMyInterface.create(constructorParameter1, constructorParameter2)
component.method()

Функция create (она сгенерирована через декоратор myinterfacedecorator) обратиться к фабрике и передаст свой тип (интерфейса) в неё с запросом на создание имплементации. Фабрика проверит наличие соответствия этому интерфейсу класса имплементации, после чего создаст прокси с имплементацией внутри (если такового нет то выкенет exception)

P.s.
Знаю что уже существуют либы реализующие подобный функционал. Но есть рестрикшены, что не позволяют их использовать.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
longclaps
@longclaps
abstractmethod
Логика немного другая, но NotImplementedError ты получишь )
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы