L'implémentation d'interfaces avec des classes de base abstraites est beaucoup plus simple dans Python 3 moderne et elles servent de contrat d'interface pour les extensions de plug-ins.
Créer l'interface/la classe de base abstraite :
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)
Créez une sous-classe normale et surchargez toutes les méthodes abstraites :
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)
Vous pouvez optionnellement avoir une implémentation commune dans les méthodes abstraites comme dans create_sale_invoice()
et l'appelle avec super()
explicitement dans la sous-classe comme ci-dessus.
L'instanciation d'une sous-classe qui n'implémente pas toutes les méthodes abstraites échoue :
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
Vous pouvez également disposer de propriétés abstraites, de méthodes statiques et de méthodes de classe en combinant les annotations correspondantes avec les éléments suivants @abstractmethod
.
Les classes de base abstraites sont idéales pour mettre en œuvre des systèmes basés sur des plugins. Toutes les sous-classes importées d'une classe sont accessibles par l'intermédiaire de la fonction __subclasses__()
Ainsi, si vous chargez toutes les classes à partir d'un répertoire de plugins avec l'option importlib.import_module()
et si elles sous-classent la classe de base, vous y avez un accès direct via __subclasses__()
et vous pouvez être sûr que le contrat d'interface est appliqué pour chacun d'entre eux lors de l'instanciation.
Voici l'implémentation du chargement des plugins pour la fonction AccountingSystem
exemple ci-dessus :
...
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
Ensuite, vous pouvez accéder à l'objet plugin du système de comptabilité par le biais de la fonction AccountingSystem
classe :
>>> accountingsystem = AccountingSystem.instance()
(Inspiré par ce billet de PyMOTW-3 .)