2012-06-27 12 views
25

Come si crea un decoratore per un metodo di classe astratta in Python 2.7?Python 2.7 Combina abc.abstractmethod e classmethod

Sì, questo è simile a this question, tranne che vorrei coniugare abc.abstractmethod e classmethod, invece di staticmethod. Inoltre, sembra che abc.abstractclassmethod sia added in Python 3 (I think?), ma sto usando Google App Engine, quindi attualmente sono limitato a Python 2.7

Grazie in anticipo.

+0

Se non è possibile utilizzare abc.abstractclassmethod, come si pone il problema di come combinarlo con un altro decoratore? – abarnert

+3

Pensa di fraintendere: abc.abstractclassmethod = abc.abstractmethod + classmethod. Python 2.7 ha abstractmethod e classmethod, ma non abstractclassmethod – jchu

+1

Sto affrontando esattamente un problema simile! Buona domanda! –

risposta

24

Ecco un esempio di lavoro derivato dal codice sorgente in Python abc modulo 3.3 del:

from abc import ABCMeta 

class abstractclassmethod(classmethod): 

    __isabstractmethod__ = True 

    def __init__(self, callable): 
     callable.__isabstractmethod__ = True 
     super(abstractclassmethod, self).__init__(callable) 

class DemoABC: 

    __metaclass__ = ABCMeta 

    @abstractclassmethod 
    def from_int(cls, n): 
     return cls() 

class DemoConcrete(DemoABC): 

    @classmethod 
    def from_int(cls, n): 
     return cls(2*n) 

    def __init__(self, n): 
     print 'Initializing with', n 

Ecco come si presenta durante l'esecuzione:

>>> d = DemoConcrete(5)    # Succeeds by calling a concrete __init__() 
Initializing with 5 

>>> d = DemoConcrete.from_int(5) # Succeeds by calling a concrete from_int() 
Initializing with 10 

>>> DemoABC()      # Fails because from_int() is abstract  
Traceback (most recent call last): 
    ... 
TypeError: Can't instantiate abstract class DemoABC with abstract methods from_int 

>>> DemoABC.from_int(5)    # Fails because from_int() is not implemented 
Traceback (most recent call last): 
    ... 
TypeError: Can't instantiate abstract class DemoABC with abstract methods from_int 

Si noti che l'esempio finale fallisce perché cls() non crea un'istanza. ABCMeta impedisce l'istanziazione prematura di classi che non hanno definito tutti i metodi astratti richiesti.

Un altro modo per attivare un errore quando il from_int() metodo della classe astratta viene chiamato è di averlo sollevare un'eccezione:

class DemoABC: 

    __metaclass__ = ABCMeta 

    @abstractclassmethod 
    def from_int(cls, n): 
     raise NotImplementedError 

Il design ABCMeta non fa nessuno sforzo per evitare qualsiasi metodo astratto dall'essere chiamato su una classe non istanziata, quindi spetta a te attivare un fallimento invocando cls() come solitamente fanno i metodi di classe o sollevando un errore NotImplemented. In ogni caso, ottieni un bel fallimento pulito.

Probabilmente è tentati di scrivere un descrittore di intercettare una chiamata diretta a un metodo di classe astratta, ma che sarebbe in contrasto con il disegno complessivo di ABCMeta (che è tutto circa il controllo per i metodi necessari prima di un'istanza piuttosto rispetto a quando vengono chiamati i metodi).

+2

Buona idea controllare il codice sorgente Python, ma sfortunatamente, simile a quando scrivo 'classmethod', seguito da' abc.abstractmethod', questo non impone 'abstractmethod'. cioè posso ancora chiamare il metodo. – jchu

+1

@jchu ABCMeta riguarda la prevenzione dell'istanza della classe quando mancano metodi astratti. Non ha codice per impedire chiamate a metodi astratti su una classe non definita. Se vuoi un errore pulito in una chiamata a un metodo di classe astratta, fallo sollevare * NotImplementedError * o provare a creare un'istanza con '' cls() ''. –

+0

Quindi è un comune pattern Python per istanziare un oggetto da un metodo di classe? Per il mio caso d'uso particolare, non ha senso istanziare un oggetto all'interno del metodo di classe, che forse spiega la mia disconnessione. Ho finito col sollevare un NotImplementedError nella classe base astratta, come hai menzionato nella tua opzione alternativa. Ma in questo caso, c'è qualche differenza nel comportamento se si usa il decoratore abstractclassmethod o il decoratore classmethod (oltre alla chiarezza nell'intento)? – jchu

2

Recentemente ho riscontrato lo stesso problema. Cioè, avevo bisogno di metodi di classe astratti ma non ero in grado di usare Python 3 a causa di altri vincoli di progetto. La soluzione che ho trovato è la seguente.

abcExtend.py:

import abc 

class instancemethodwrapper(object): 
    def __init__(self, callable): 
     self.callable = callable 
     self.__dontcall__ = False 

    def __getattr__(self, key): 
     return getattr(self.callable, key) 

    def __call__(self, *args, **kwargs): 
     if self.__dontcall__: 
      raise TypeError('Attempted to call abstract method.') 
     return self.callable(*args,**kwargs) 

class newclassmethod(classmethod): 
    def __init__(self, func): 
     super(newclassmethod, self).__init__(func) 
     isabstractmethod = getattr(func,'__isabstractmethod__',False) 
     if isabstractmethod: 
      self.__isabstractmethod__ = isabstractmethod 

    def __get__(self, instance, owner): 
     result = instancemethodwrapper(super(newclassmethod, self).__get__(instance, owner)) 
     isabstractmethod = getattr(self,'__isabstractmethod__',False) 
     if isabstractmethod: 
      result.__isabstractmethod__ = isabstractmethod 
      abstractmethods = getattr(owner,'__abstractmethods__',None) 
      if abstractmethods and result.__name__ in abstractmethods: 
       result.__dontcall__ = True 
     return result 

class abstractclassmethod(newclassmethod): 
    def __init__(self, func): 
     func = abc.abstractmethod(func) 
     super(abstractclassmethod,self).__init__(func) 

Usage:

from abcExtend import abstractclassmethod 

class A(object): 
    __metaclass__ = abc.ABCMeta  
    @abstractclassmethod 
    def foo(cls): 
     return 6 

class B(A): 
    pass 

class C(B): 
    @classmethod 
    def foo(cls): 
     return super(C,cls).foo() + 1 

try: 
    a = A() 
except TypeError: 
    print 'Instantiating A raises a TypeError.' 

try: 
    A.foo() 
except TypeError: 
    print 'Calling A.foo raises a TypeError.' 

try: 
    b = B() 
except TypeError: 
    print 'Instantiating B also raises a TypeError because foo was not overridden.' 

try: 
    B.foo() 
except TypeError: 
    print 'As does calling B.foo.' 

#But C can be instantiated because C overrides foo 
c = C() 

#And C.foo can be called 
print C.foo() 

e qui ci sono alcune prove PyUnit che danno una dimostrazione più esaustivo.

testAbcExtend.py:

import unittest 
import abc 
oldclassmethod = classmethod 
from abcExtend import newclassmethod as classmethod, abstractclassmethod 

class Test(unittest.TestCase): 
    def setUp(self): 
     pass 

    def tearDown(self): 
     pass 

    def testClassmethod(self): 
     class A(object): 
      __metaclass__ = abc.ABCMeta    
      @classmethod 
      @abc.abstractmethod 
      def foo(cls): 
       return 6 

     class B(A): 
      @classmethod 
      def bar(cls): 
       return 5 

     class C(B): 
      @classmethod 
      def foo(cls): 
       return super(C,cls).foo() + 1 

     self.assertRaises(TypeError,A.foo) 
     self.assertRaises(TypeError,A) 
     self.assertRaises(TypeError,B) 
     self.assertRaises(TypeError,B.foo) 
     self.assertEqual(B.bar(),5) 
     self.assertEqual(C.bar(),5) 
     self.assertEqual(C.foo(),7) 

    def testAbstractclassmethod(self): 
     class A(object): 
      __metaclass__ = abc.ABCMeta  
      @abstractclassmethod 
      def foo(cls): 
       return 6 

     class B(A): 
      pass 

     class C(B): 
      @oldclassmethod 
      def foo(cls): 
       return super(C,cls).foo() + 1 

     self.assertRaises(TypeError,A.foo) 
     self.assertRaises(TypeError,A) 
     self.assertRaises(TypeError,B) 
     self.assertRaises(TypeError,B.foo) 
     self.assertEqual(C.foo(),7) 
     c = C() 
     self.assertEqual(c.foo(),7) 

if __name__ == "__main__": 
    #import sys;sys.argv = ['', 'Test.testName'] 
    unittest.main() 

non ho valutato il costo delle prestazioni di questa soluzione, ma ha funzionato per i miei scopi finora.

14

Un'altra possibile soluzione:

class A: 
    __metaclass__ = abc.ABCMeta 

    @abc.abstractmethod 
    def some_classmethod(cls): 
     """IMPORTANT: this is class method, override it with @classmethod!""" 
     pass 

class B(A): 
    @classmethod 
    def some_classmethod(cls): 
     print cls 

Ora, uno ancora non si può istanziare da A fino a 'some_classmethod' viene implementato, e funziona se si implementa con un classmethod.

+0

Questa dovrebbe essere la risposta migliore –