web-dev-qa-db-fra.com

Attribut abstrait (pas de propriété)?

Quelle est la meilleure pratique pour définir un attribut d'instance abstraite, mais pas en tant que propriété?

J'aimerais écrire quelque chose comme:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

Au lieu de:

class Foo(AbstractFoo):

    def __init__(self):
        self._bar = 3

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def setbar(self, bar):
        self._bar = bar

    @bar.deleter
    def delbar(self):
        del self._bar

Les propriétés sont pratiques, mais pour un attribut simple ne nécessitant aucun calcul, elles sont excessives. Ceci est particulièrement important pour les classes abstraites qui seront sous-classées et implémentées par l'utilisateur (je ne veux pas forcer quelqu'un à utiliser @property alors qu'il aurait pu écrire self.foo = foo dans le __init__).

Attributs abstraits en Python question propose comme seule réponse d'utiliser @property et @abstractmethod: il ne répond pas à ma question.

La recette ActiveState pour un attribut de classe abstrait via AbstractAttribute est peut-être la bonne façon, mais je ne suis pas sûr. Il ne fonctionne également qu'avec les attributs de classe et non les attributs d'instance.

39
Lapinot

Si vous voulez vraiment imposer à une sous-classe de définir un attribut donné, vous pouvez utiliser une métaclasse. Personnellement, je pense que c'est peut-être excessif et pas très pythonique, mais vous pourriez faire quelque chose comme ça:

 class AbstractFooMeta(type):

     def __call__(cls, *args, **kwargs):
         """Called when you call Foo(*args, **kwargs) """
         obj = type.__call__(cls, *args, **kwargs)
         obj.check_bar()
         return obj


 class AbstractFoo(object):
     __metaclass__ = AbstractFooMeta
     bar = None

     def check_bar(self):
         if self.bar is None:
             raise NotImplementedError('Subclasses must define bar')


 class GoodFoo(AbstractFoo):
     def __init__(self):
         self.bar = 3


 class BadFoo(AbstractFoo):
     def __init__(self):
         pass

En gros, la méta classe redéfinit __call__ pour s'assurer que check_bar est appelé après l'init sur une instance.

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError
17

Ce n’est pas parce que vous définissez une variable abstractproperty sur la classe de base abstraite que vous devez créer une propriété sur la sous-classe.

par exemple. vous pouvez:

In [1]: from abc import ABCMeta, abstractproperty

In [2]: class X(metaclass=ABCMeta):
   ...:     @abstractproperty
   ...:     def required(self):
   ...:         raise NotImplementedError
   ...:

In [3]: class Y(X):
   ...:     required = True
   ...:

In [4]: Y()
Out[4]: <__main__.Y at 0x10ae0d390>

Si vous voulez initialiser la valeur dans __init__, vous pouvez faire ceci:

In [5]: class Z(X):
   ...:     required = None
   ...:     def __init__(self, value):
   ...:         self.required = value
   ...:

In [6]: Z(value=3)
Out[6]: <__main__.Z at 0x10ae15a20>
11
Anentropic

Le problème n'est pas what, mais when:

from abc import ABCMeta, abstractmethod

class AbstractFoo(metaclass=ABCMeta):
    @abstractmethod
    def bar():
        pass

class Foo(AbstractFoo):
    bar = object()

isinstance(Foo(), AbstractFoo)
#>>> True

Peu importe que bar ne soit pas une méthode! Le problème est que __subclasshook__, la méthode de vérification, est un classmethod, aussi ne se soucie-t-il que si la classe, et non la instance, a l'attribut.


Je vous suggère de ne pas forcer, car c'est un problème difficile. L'alternative est de les forcer à prédéfinir l'attribut, mais cela ne laisse que des attributs factices qui font taire les erreurs.

7
Veedrac

Nous sommes en 2018, nous méritons une solution un peu meilleure:

from better_abc import ABCMeta, abstract_attribute    # see below

class AbstractFoo(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self):
        pass

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = 3

class BadFoo(AbstractFoo):
    def __init__(self):
        pass

Il se comportera comme ceci:

Foo()     # ok
BadFoo()  # will raise: NotImplementedError: Can't instantiate abstract class BadFoo
# with abstract attributes: bar

Cette réponse utilise la même approche que la réponse acceptée, mais s'intègre bien à l'ABC intégré et ne nécessite pas de passe-partout "check_bar()".

Voici le contenu de better_abc.py:

from abc import ABCMeta as NativeABCMeta

class DummyAttribute:
    pass

def abstract_attribute(obj=None):
    if obj is None:
        obj = DummyAttribute()
    obj.__is_abstract_attribute__ = True
    return obj


class ABCMeta(NativeABCMeta):

    def __call__(cls, *args, **kwargs):
        instance = NativeABCMeta.__call__(cls, *args, **kwargs)
        abstract_attributes = {
            name
            for name in dir(instance)
            if getattr(getattr(instance, name), '__is_abstract_attribute__', False)
        }
        if abstract_attributes:
            raise NotImplementedError(
                "Can't instantiate abstract class {} with"
                " abstract attributes: {}".format(
                    cls.__name__,
                    ', '.join(abstract_attributes)
                )
            )
        return instance

La bonne chose est que vous pouvez faire:

class AbstractFoo(metaclass=ABCMeta):
    bar = abstract_attribute()

et cela fonctionnera comme ci-dessus.

Aussi on peut utiliser:

class ABC(ABCMeta):
    pass

définir un assistant ABC personnalisé. PS. Je considère que ce code est CC0.

Cela pourrait être amélioré en utilisant un analyseur syntaxique AST pour lever plus tôt (sur la déclaration de classe) en scannant le code __init__, mais cela semble excessif pour le moment (sauf si quelqu'un est disposé à l'implémenter). 

6
krassowski

Après https://docs.python.org/2/library/abc.html vous pourriez faire quelque chose comme ceci dans Python 2.7:

from abc import ABCMeta, abstractproperty


class Test(object):
    __metaclass__ = ABCMeta

    @abstractproperty
    def test(self): yield None

    def get_test(self):
        return self.test


class TestChild(Test):

    test = None

    def __init__(self, var):
        self.test = var


a = TestChild('test')
print(a.get_test())
0
Javier Montón

J'ai cherché cela pendant un moment mais je n'ai rien vu qui me plaise. Comme vous le savez probablement si vous le faites:

class AbstractFoo(object):
    @property
    def bar(self):
        raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = "bar"

f = Foo()

Vous obtenez un AttributeError: can't set attribute qui est ennuyeux.

Pour contourner cela, vous pouvez faire:

class AbstractFoo(object):

    @property
    def bar(self):
        try:
            return self._bar
        except AttributeError:
            raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class OkFoo(AbstractFoo):
    def __init__(self):
        self._bar = 3

class BadFoo(AbstractFoo):
    pass

a = OkFoo()
b = BadFoo()
print a.bar
print b.bar  # raises a NotImplementedError

Ceci évite le AttributeError: can't set attribute mais si vous laissez la propriété abstraite au complet:

class AbstractFoo(object):
    pass

class Foo(AbstractFoo):
    pass

f = Foo()
f.bar

Vous obtenez un AttributeError: 'Foo' object has no attribute 'bar' qui est sans doute presque aussi bon que le NotImplementedError. Donc, vraiment, ma solution consiste à échanger un message d'erreur d'un autre .. et vous devez utiliser self._bar plutôt que self.bar dans le init.

0
James Irwin