Prenons l'exemple minimal suivant:
import abc
class FooClass(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def FooMethod(self):
raise NotImplementedError()
def main():
derived_type = type('Derived', (FooClass,), {})
def BarOverride(self):
print 'Hello, world!'
derived_type.FooMethod = BarOverride
instance = derived_type()
L'exécution de main()
vous permet:
TypeError: Can't instantiate abstract class Derived with abstract methods FooMethod
(L'exception se produit sur la ligne instance = derived_type()
.)
Mais FooMethod
ne doit pas être abstrait: je l'ai remplacé par BarOverride
. Alors, pourquoi cela soulève-t-il des exceptions?
Avertissement: Oui, je pourrais utiliser la syntaxe explicite class
et accomplir exactement la même chose. (Et encore mieux, je peux le faire fonctionner!) Mais il s'agit d'un cas de test minimal, et l'exemple le plus important est la création dynamique de classes. :-) Et je suis curieux de savoir pourquoi cela ne fonctionne pas.
Modifier: Et pour éviter l'autre non-réponse évidente: je ne veux pas passer BarOverride
dans le troisième argument à type
: dans l'exemple réel, {{X2 }} doit être lié derived_type
. C'est plus facile à faire si je peux définir BarOverride
après la création de derived_type
. (Si je ne peux pas faire ça, alors pourquoi?)
4 réponses
L'ajout dynamique de méthodes abstraites à une classe ou la tentative de modification de l'état d'abstraction d'une méthode ou d'une classe une fois qu'elle est créée ne sont pas pris en charge. La méthode abstractmethod () n'affecte que les sous-classes dérivées en utilisant l'héritage régulier; les "sous-classes virtuelles" enregistrées avec la méthode register () de l'ABC ne sont pas affectées.
Une métaclasse n'est appelée que lorsqu'une classe est définie. Lorsque abstractmethod
a marqué une classe comme abstraite, ce statut ne changera pas plus tard.
Eh bien, si vous devez le faire de cette façon, vous pouvez simplement passer un dict factice {'FooMethod':None}
comme troisième argument à taper. Cela permet à derived_type
de satisfaire à l'exigence d'ABCMeta que toutes les méthodes abstraites soient remplacées. Plus tard, vous pourrez fournir le véritable FooMethod
:
def main():
derived_type = type('Derived', (FooClass,), {'FooMethod':None})
def BarOverride(self):
print 'Hello, world!'
setattr(derived_type, 'FooMethod', BarOverride)
instance = derived_type()
Je sais que ce sujet est vraiment ancien mais ... C'est vraiment une bonne question.
Cela ne fonctionne pas car abc ne peut rechercher des méthodes abstraites que lors de l'instanciation de types, c'est-à-dire lorsque type('Derived', (FooClass,), {})
est en cours d'exécution. Tout getattr effectué après cela n'est pas accessible depuis abc.
Donc, setattr ne fonctionnera pas, buuut ... Votre problème d'adressage du nom d'une classe qui n'a pas été précédemment déclarée ou définie semble résoluble:
J'ai écrit une petite métaclasse qui vous permet d'utiliser un espace réservé "clazz" pour accéder à n'importe quelle classe qui obtiendra éventuellement la méthode que vous écrivez en dehors d'une définition de classe.
De cette façon, vous n'obtiendrez plus TypeError de abc, car vous pouvez maintenant définir votre méthode AVANT d'instituer votre type, puis la transmettre à type à l'argument dict. Ensuite, abc le verra comme un remplacement de méthode approprié.
Aaand, avec la nouvelle métaclasse, vous pouvez faire référence à l'objet classe pendant cette méthode. Et c'est super, car maintenant vous pouvez utiliser super! = P Je suppose que vous étiez aussi inquiet à ce sujet ...
Regarde:
import abc
import inspect
clazz = type('clazz', (object,), {})()
def clazzRef(func_obj):
func_obj.__hasclazzref__ = True
return func_obj
class MetaClazzRef(type):
"""Makes the clazz placeholder work.
Checks which of your functions or methods use the decorator clazzRef
and swaps its global reference so that "clazz" resolves to the
desired class, that is, the one where the method is set or defined.
"""
methods = {}
def __new__(mcs, name, bases, dict):
ret = super(MetaClazzRef, mcs).__new__(mcs, name, bases, dict)
for (k,f) in dict.items():
if getattr(f, '__hasclazzref__', False):
if inspect.ismethod(f):
f = f.im_func
if inspect.isfunction(f):
for (var,value) in f.func_globals.items():
if value is clazz:
f.func_globals[var] = ret
return ret
class MetaMix(abc.ABCMeta, MetaClazzRef):
pass
class FooClass(object):
__metaclass__ = MetaMix
@abc.abstractmethod
def FooMethod(self):
print 'Ooops...'
#raise NotImplementedError()
def main():
@clazzRef
def BarOverride(self):
print "Hello, world! I'm a %s but this method is from class %s!" % (type(self), clazz)
super(clazz, self).FooMethod() # Now I have SUPER!!!
derived_type = type('Derived', (FooClass,), {'FooMethod': BarOverride})
instance = derived_type()
instance.FooMethod()
class derivedDerived(derived_type):
def FooMethod(self):
print 'I inherit from derived.'
super(derivedDerived,self).FooMethod()
instance = derivedDerived()
instance.FooMethod()
main()
La sortie est:
Hello, world! I'm a <class 'clazz.Derived'> but this method is from class <class 'clazz.Derived'>!
Ooops...
I inherit from derived.
Hello, world! I'm a <class 'clazz.derivedDerived'> but this method is from class <class 'clazz.Derived'>!
Ooops...
Jochen a raison; les méthodes abstraites sont définies lors de la création de la classe et ne seront pas modifiées simplement parce que vous réaffectez un attribut.
Vous pouvez le supprimer manuellement de la liste des méthodes abstraites en faisant
DerivedType.__abstractmethods__ = frozenset()
Ou
DerivedType.__abstractmethods__ = frozenset(
elem for elem in DerivedType.__abstractmethods__ if elem != 'FooMethod')
Ainsi que setattr
, il ne pense donc pas que FooMethod
soit abstrait.
Questions connexes
De nouvelles questions
python
Python est un langage de programmation multi-paradigme, typé dynamiquement et polyvalent. Il est conçu pour être rapide à apprendre, comprendre, utiliser et appliquer une syntaxe propre et uniforme. Veuillez noter que Python 2 est officiellement hors support à partir du 01-01-2020. Néanmoins, pour les questions Python spécifiques à la version, ajoutez la balise [python-2.7] ou [python-3.x]. Lorsque vous utilisez une variante Python (par exemple, Jython, PyPy) ou une bibliothèque (par exemple, Pandas et NumPy), veuillez l'inclure dans les balises.