A implementação de interfaces com classes base abstratas é muito mais simples no Python 3 moderno e serve a um propósito como contrato de interface para extensões de plug-in.
Crie a interface / classe base abstrata:
from abc import ABC, abstractmethod
class AccountingSystem(ABC):
@abstractmethod
def create_purchase_invoice(self, purchase):
pass
@abstractmethod
def create_sale_invoice(self, sale):
log.debug('Creating sale invoice', sale)
Crie uma subclasse normal e substitua todos os métodos abstratos:
class GizmoAccountingSystem(AccountingSystem):
def create_purchase_invoice(self, purchase):
submit_to_gizmo_purchase_service(purchase)
def create_sale_invoice(self, sale):
super().create_sale_invoice(sale)
submit_to_gizmo_sale_service(sale)
Opcionalmente, você pode ter uma implementação comum nos métodos abstratos como em create_sale_invoice()
, chamando-a super()
explicitamente na subclasse como acima.
Falha na instanciação de uma subclasse que não implementa todos os métodos abstratos:
class IncompleteAccountingSystem(AccountingSystem):
pass
>>> accounting = IncompleteAccountingSystem()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class IncompleteAccountingSystem with abstract methods
create_purchase_invoice, create_sale_invoice
Você também pode ter propriedades abstratas, métodos estáticos e de classe combinando anotações correspondentes com @abstractmethod
.
As classes base abstratas são ótimas para implementar sistemas baseados em plugins. Todas as subclasses importadas de uma classe são acessíveis via __subclasses__()
, portanto, se você carregar todas as classes de um diretório de plug-ins importlib.import_module()
e se elas subclassificarem a classe base, você terá acesso direto a elas via __subclasses__()
e poderá ter certeza de que o contrato de interface é aplicado a todos eles durante a instanciação.
Aqui está a implementação de carregamento do plug-in para o AccountingSystem
exemplo acima:
...
from importlib import import_module
class AccountingSystem(ABC):
...
_instance = None
@classmethod
def instance(cls):
if not cls._instance:
module_name = settings.ACCOUNTING_SYSTEM_MODULE_NAME
import_module(module_name)
subclasses = cls.__subclasses__()
if len(subclasses) > 1:
raise InvalidAccountingSystemError('More than one '
f'accounting module: {subclasses}')
if not subclasses or module_name not in str(subclasses[0]):
raise InvalidAccountingSystemError('Accounting module '
f'{module_name} does not exist or does not '
'subclass AccountingSystem')
cls._instance = subclasses[0]()
return cls._instance
Em seguida, você pode acessar o objeto de plug-in do sistema de contabilidade por meio da AccountingSystem
classe:
>>> accountingsystem = AccountingSystem.instance()
(Inspirado por este post do PyMOTW-3 .)