Quel est le moyen le plus court/le plus élégant d'implémenter le code Scala suivant avec un attribut abstrait en Python?
abstract class Controller {
val path: String
}
Une sous-classe de Controller
est imposée par le compilateur Scala pour définir "chemin". Une sous-classe ressemblerait à ceci:
class MyController extends Controller {
override val path = "/home"
}
Python a une exception intégrée pour cela, bien que vous ne la rencontriez pas avant le runtime.
class Base(object):
@property
def path(self):
raise NotImplementedError
class SubClass(Base):
path = 'blah'
from abc import ABCMeta, abstractmethod
class A(metaclass=ABCMeta):
def __init__(self):
# ...
pass
@property
@abstractmethod
def a(self):
pass
@abstractmethod
def b(self):
pass
class B(A):
a = 1
def b(self):
pass
Si vous ne déclarez pas a
ou b
dans la classe dérivée B
, une TypeError
sera générée, par exemple:
TypeError
: Impossible d'instancier la classe abstraiteB
avec des méthodes abstraitesa
Il existe un @ abstractproperty decorator pour cela:
from abc import ABCMeta, abstractmethod, abstractproperty
class A:
__metaclass__ = ABCMeta
def __init__(self):
# ...
pass
@abstractproperty
def a(self):
pass
@abstractmethod
def b(self):
pass
class B(A):
a = 1
def b(self):
pass
Vous pouvez créer un attribut dans la classe de base abc.ABC abstract avec une valeur telle que NotImplemented
de sorte que si l'attribut n'est pas remplacé, il est utilisé, une erreur est affichée au moment de l'exécution.
Le code suivant utilise un indicateur de type PEP 484 pour aider PyCharm à analyser correctement, de manière statique, le type de l'attribut path
.
import abc
class Controller(abc.ABC):
path = NotImplemented # type: str
class MyController(Controller):
path = '/home'
Consultez le module abc (Abtract Base Class): http://docs.python.org/library/abc.html
Cependant, à mon avis, la solution la plus simple et la plus courante consiste à déclencher une exception lors de la création d'une instance de la classe de base ou lors de l'accès à sa propriété.
Depuis que cette question a été posée à l'origine, python a changé la manière dont les classes abstraites sont implémentées. J'ai utilisé une approche légèrement différente en utilisant le formalisme abc.ABC dans Python 3.6. Ici, je définis la constante comme une propriété qui doit être définie dans chaque sous-classe.
from abc import ABC, abstractmethod
class Base(ABC):
@property
@classmethod
@abstractmethod
def CONSTANT(cls):
return NotImplementedError
def print_constant(self):
print(type(self).CONSTANT)
class Derived(Base):
CONSTANT = 42
Cela force la classe dérivée à définir la constante, sinon une exception TypeError
sera déclenchée lorsque vous essayez d’instancier la sous-classe. Lorsque vous souhaitez utiliser la constante pour toute fonctionnalité implémentée dans la classe abstraite, vous devez accéder à la constante de sous-classe par type(self).CONSTANT
au lieu de simplement CONSTANT
, car la valeur n'est pas définie dans la classe de base.
Il y a d'autres moyens d'implémenter cela, mais j'aime bien cette syntaxe, car elle me semble la plus simple et la plus évidente pour le lecteur.
Les réponses précédentes touchaient toutes des points utiles, mais j'estime que la réponse acceptée ne répond pas directement à la question car
CONSTANT
n'est pas défini. La réponse acceptée permet l'instanciation de l'objet et ne génère une erreur que lorsque l'accès à CONSTANT
a pour effet de rendre l'application moins stricte. Ce n'est pas à redire aux réponses originales. Des changements importants ont été apportés à la syntaxe de la classe abstraite depuis leur publication, ce qui permet dans ce cas une implémentation plus nette et plus fonctionnelle.
Votre classe de base pourrait implémenter une méthode __new__
qui vérifie l'attribut de classe:
class Controller(object):
def __new__(cls, *args, **kargs):
if not hasattr(cls,'path'):
raise NotImplementedError("'Controller' subclasses should have a 'path' attribute")
return object.__new__(cls,*args,**kargs)
class C1(Controller):
path = 42
class C2(Controller):
pass
c1 = C1()
# ok
c2 = C2()
# NotImplementedError: 'Controller' subclasses should have a 'path' attribute
De cette façon, l'erreur augmente à l'instanciation
L'implémentation de Python3.6 pourrait ressembler à ceci:
In [20]: class X:
...: def __init_subclass__(cls):
...: if not hasattr(cls, 'required'):
...: raise NotImplementedError
In [21]: class Y(X):
...: required =5
...:
In [22]: Y()
Out[22]: <__main__.Y at 0x7f08408c9a20>
class AbstractStuff:
@property
@abc.abstractmethod
def some_property(self):
pass
À partir de la version 3.3, abc.abstractproperty
est obsolète, je pense.
Dans Python 3.6+ , vous pouvez annoter un attribut d'une classe abstraite (ou de toute variable) sans fournir de valeur pour cet attribut.
class Controller:
path: str
class MyController(Controller):
path = "/home"
Cela donne un code très propre où il est évident que l'attribut est abstrait. Le code qui tente d'accéder à l'attribut alors que s'il n'a pas été écrasé déclenche une variable AttributeError
.
La réponse de Bastien Léonard mentionne le module de la classe de base abstraite et la réponse de Brendan Abel concerne les attributs non implémentés qui génèrent des erreurs. Pour vous assurer que la classe n'est pas implémentée en dehors du module, vous pouvez préfixer le nom de base par un trait de soulignement qui le désigne comme étant privé du module (c'est-à-dire qu'il n'est pas importé).
c'est à dire.
class _Controller(object):
path = '' # There are better ways to declare attributes - see other answers
class MyController(_Controller):
path = '/Home'