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.
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
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>
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.
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).
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())
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.