2012-06-27 22 views
25

¿Cómo creo un decorador para un método de clase abstracta en Python 2.7?Python 2.7 Combine abc.abstractmethod y classmethod

Sí, esto es similar a this question, excepto que me gustaría combinar abc.abstractmethod y classmethod, en lugar de staticmethod. Además, parece que abc.abstractclassmethod era added in Python 3 (I think?), pero estoy usando Google App Engine, por lo que actualmente estoy limitado a Python 2.7

Gracias de antemano.

+0

Si no puede usar abc.abstractclassmethod, ¿cómo surge la cuestión de cómo combinarlo con otro decorador? – abarnert

+3

Creo que leyó mal: abc.abstractclassmethod = abc.abstractmethod + classmethod. Python 2.7 tiene abstract method y classmethod, pero no abstractclassmethod – jchu

+1

¡Estoy enfrentando un problema exactamente similar! ¡Buena pregunta! –

Respuesta

24

Aquí está un ejemplo de trabajo derivado del código fuente en Python 3.3 abc módulo:

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 

Esto es lo que parece cuando se ejecuta:

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

Tenga en cuenta que el último ejemplo falla porque cls() no instanciará. ABCMeta previene la creación de instancias prematuras de clases que no han definido todos los métodos abstractos requeridos.

otra manera de accionar un fracaso cuando el from_int() método de la clase abstracta se llama es tener que elevar una excepción:

class DemoABC: 

    __metaclass__ = ABCMeta 

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

El diseño ABCMeta no hace ningún esfuerzo para evitar cualquier método abstracto de ser llamado a una clase desinstalada, por lo que depende de usted desencadenar una falla invocando cls() como lo hace generalmente classmethods o planteando NotImplementedError. De cualquier manera, obtienes un fracaso agradable y limpio.

Probablemente es tentador para escribir un descriptor para interceptar una llamada directa a un método de la clase abstracta, sino que estaría en contradicción con el diseño general de ABCMeta (que es todo acerca de la comprobación de los métodos necesarios antes de la creación de instancias en vez que cuando se llaman los métodos).

+2

Buena idea para verificar el código fuente de Python, pero desafortunadamente, similar a cuando escribo 'classmethod', seguido de' abc.abstractmethod', esto no aplica 'abstractmethod'. es decir, todavía puedo llamar al método. – jchu

+1

@jchu ABCMeta se trata de evitar la creación de instancias de clase cuando faltan métodos abstractos. No tiene código para evitar llamadas a métodos abstractos en una clase desinstalada. Si desea una falla limpia en una llamada a un método de clase abstracta, haga que aumente * NotImplementedError * o intente crear una instancia con '' cls() ''. –

+0

¿Es un patrón común de Python instanciar un objeto de un método de clase? Para mi caso de uso particular, no tiene sentido crear una instancia de un objeto dentro del método de la clase, lo que tal vez explique mi desconexión. Terminé levantando un NotImplementedError en la clase base abstracta, como mencionaste en tu opción alternativa. Pero en ese caso, ¿hay alguna diferencia en el comportamiento si usaste el decorador de métodos de clase abstracta o el decorador de métodos de clase (además de la claridad en la intención)? – jchu

2

Recientemente he encontrado el mismo problema. Es decir, necesitaba métodos de clase abstractos pero no pude usar Python 3 debido a otras limitaciones del proyecto. La solución que se me ocurrió es la siguiente.

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) 

Uso:

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

Y aquí están algunas pruebas PyUnit que dan una demostración más exhaustiva.

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

no he evaluado el costo de rendimiento de esta solución, pero ha trabajado para mis propósitos hasta el momento.

14

Otra posible solución:

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 

Ahora, todavía no se puede crear una instancia de la A hasta 'some_classmethod' se implementa, y funciona si se implementa con un classmethod.

+0

Esta debería ser la respuesta principal –