2011-11-10 4 views
7

Prendiamo il seguente esempio minimale:“Impossibile un'istanza di classe astratta ... con metodi astratti” a classe che non dovrebbe avere alcun metodo astratto

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

esecuzione main() si ottiene:

TypeError: Can't instantiate abstract class Derived with abstract methods FooMethod 

(L'eccezione si verifica sulla riga instance = derived_type().)

Ma FooMethod non deve essere astratto: l'ho sovrascritto con BarOverride. Quindi, perché questo fa sorgere eccezioni?

Disclaimer: Sì, potrei usare la sintassi esplicita class e realizzare esattamente la stessa cosa. (E ancora meglio, posso farlo funzionare!) Ma questo è un caso di test minimale, e l'esempio più ampio è la creazione dinamica di classi. :-) E sono curioso di sapere perché questo non funziona.

Edit: E per evitare che l'altra ovvia non-risposta: io non voglio passare BarOverride nel terzo argomento di type: Nell'esempio vera, BarOverride ha bisogno di avere derived_type legato ad esso. È più semplice farlo se riesco a definire BarOverride dopo la creazione di derived_type. (Se non riesco a fare questo, allora perché?)

+1

L'astrattezza di una classe è determinata durante la costruzione di classe . Perché non inserisci semplicemente 'FooMethod' nel dizionario mentre crei la classe? –

+0

@SvenMarnach: 'BarOverride', nell'esempio reale, deve avere la classe derivata vincolata ad esso. Creare la funzione in seguito è il modo più semplice per farlo. – Thanatos

+0

Non capisco quel punto. Cosa intendi per "necessità di avere la classe derivata vincolata ad esso"? –

risposta

4

Because the docs say so:

dinamicamente l'aggiunta di metodi astratti a una classe, o il tentativo di modificare lo stato astrazione di un metodo o di classe una volta che è creato, non sono supportati. Il metodo abstract() riguarda solo le sottoclassi derivate dall'ereditarietà normale; "Sottoclassi virtuali" registrati con il metodo register() dell'ABC non sono interessati.

Un metaclasse viene chiamato solo quando viene definita una classe. Quando abstractmethod ha contrassegnato una classe come astratta, lo stato non cambierà più avanti.

+0

Accettare questo come risposta (perché contiene il motivo per cui il mio codice non funziona), ma tutti e tre sono interessanti. Ho selezionato la risposta di unutbu come soluzione alternativa al problema in questione. – Thanatos

2

Bene, se è necessario farlo in questo modo, è possibile passare un dumt fittizio {'FooMethod':None} come terzo argomento da digitare. Ciò consente a derived_type di soddisfare il requisito di ABCMeta che tutti i metodi astratti siano sovrascritti. In seguito è possibile fornire la vera FooMethod:

def main(): 
    derived_type = type('Derived', (FooClass,), {'FooMethod':None}) 
    def BarOverride(self): 
    print 'Hello, world!' 
    setattr(derived_type, 'FooMethod', BarOverride) 
    instance = derived_type() 
+0

Ma questo vanifica un po 'del punto di rendere astratto 'FooMethod', perché se la creazione del tipo e' setattr' sono separati, puoi istanziare 'derived_type' prima di impostare il metodo. – agf

+0

Una classe di tipo 'ABCMeta' dovrebbe essere scimmia-patchable come qualsiasi altra classe. Fornendo il terzo argomento si riconosce che "Derivato" deve definire "FooMethod". Ciò non ti impedisce di cambiare idea sulla sua implementazione in seguito. – unutbu

3

Jochen è giusto; i metodi astratti sono impostati alla creazione della classe e non verranno modificati solo perché riassegni un attributo.

È possibile rimuovere manualmente dall'elenco dei metodi astratti facendo

DerivedType.__abstractmethods__ = frozenset() 

o

DerivedType.__abstractmethods__ = frozenset(
     elem for elem in DerivedType.__abstractmethods__ if elem != 'FooMethod') 

così come setattr, quindi non fa ancora pensare che FooMethod è astratta.

3

So che questo argomento è davvero vecchio ma ... Questa è davvero una bella domanda.

Non funziona perché abc può solo verificare i metodi astratti durante l'instatia dei tipi, ovvero quando type('Derived', (FooClass,), {}) è in esecuzione. Qualsiasi setattr fatto dopo non è accessibile da abc.

Quindi, non funzionerà setattr, buuut ... Il tuo problema di affrontare il nome di una classe non precedentemente dichiarato o definito sembra risolvibile:

ho scritto un po 'metaclasse che consente di utilizzare un segnaposto "clazz" per accedere a qualsiasi classe che alla fine otterrà il metodo che stai scrivendo al di fuori di una definizione di classe.

In questo modo non otterrete più TypeError da abc, poiché ora potete definire il metodo PRIMA di instatizzare il vostro tipo e quindi passarlo per digitare all'argomento dict. Quindi abc lo vedrà come un metodo appropriato di override.

Aaand, con il nuovo metaclasse è possibile fare riferimento all'oggetto classe durante tale metodo. E questo è super, perché ora puoi usare super! = P posso indovinare si erano preoccupati anche su questo ...

Date un'occhiata:

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

L'output è:

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... 
+0

Immagino che questo sia sbagliato. Dovrei farlo con le chiusure di funzioni, non globali(). Globals() sono, err, globale ... Ma, stranamente, il codice sembra funzionare come previsto, anche con più classi ... – rbertoche