web-dev-qa-db-fra.com

Attributs abstraits en Python

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"

}
31
deamon

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'
49
user297250

Python 3.3+

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 abstraite B avec des méthodes abstraites a

Python 2.7

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
49
Wtower

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'
5
phoenix

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é.

4
Bastien Léonard

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

  • La question demande une implémentation dans une classe abstraite, mais la réponse acceptée ne suit pas le formalisme abstrait.
  • La question demande que la mise en œuvre soit appliquée. Je dirais que l'application est plus stricte dans cette réponse car elle provoque une erreur d'exécution lorsque la sous-classe est instanciée si 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. 

4
James

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

3
Juh_

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>
1
Artem Zhukov
class AbstractStuff:
    @property
    @abc.abstractmethod
    def some_property(self):
        pass

À partir de la version 3.3, abc.abstractproperty est obsolète, je pense.

0
grisaitis

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.

0
drhagen

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'
0
Brendan