Python 2.7 Combine abc.abstractmethod and classmethod



如何在Python 2.7中为抽象类方法创建装饰器?

是的,这与这个问题类似,只是我想将abc.abstractmethodclassmethod结合起来,而不是staticmethod. 另外,看起来abc.abstractclassmethod是在Python 3中添加的(我认为?),但我使用的是Google App Engine,所以我目前仅限于Python 2.7

提前谢谢。

下面是一个从 Python 3.3 的 abc 模块中的源代码派生的工作示例:

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

下面是运行时的外观:

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

请注意,最后一个示例失败,因为cls()不会实例化。 ABCMeta 可防止过早实例化尚未定义所有必需抽象方法的类。

调用 from_int() 抽象类方法时触发失败的另一种方法是让它引发异常:

class DemoABC:
    __metaclass__ = ABCMeta
    @abstractclassmethod
    def from_int(cls, n):
        raise NotImplementedError

设计 ABCMeta 不会努力阻止在未实例化的类上调用任何抽象方法,因此您可以通过像类方法通常那样调用cls()或通过引发 NotImplementError 来触发失败。 无论哪种方式,你都会得到一个漂亮的、干净的失败。

编写一个描述符来拦截对抽象类方法的直接调用可能很诱人,但这与 ABCMeta 的整体设计不一致(这完全是在实例化之前检查所需的方法,而不是在调用方法时

)。

你可以升级到 Python 3

Python 3.3 开始,可以将 @classmethod@abstractmethod 结合起来:

import abc
class Foo(abc.ABC):
    @classmethod
    @abc.abstractmethod
    def my_abstract_classmethod(...):
        pass

感谢@gerrit向我指出这一点。

另一种可能的解决方法:

class A:
    __metaclass__ = abc.ABCMeta
    
    @abc.abstractmethod
    def some_classmethod(cls):
        """IMPORTANT: this is a class method, override it with @classmethod!"""
class B(A):
    @classmethod
    def some_classmethod(cls):
        print cls

现在,在实现some_classmethod之前,仍然无法从A实例化,如果您使用@classmethod实现它,它可以工作。

我最近遇到了同样的问题。 也就是说,我需要抽象类方法,但由于其他项目约束而无法使用Python 3。 我想出的解决方案如下。

abc-extend.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)

用法:

from abc-extend 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()

这里有一些pyunit测试,它们给出了更详尽的演示。

test-abc-extend.py:

import unittest
import abc
oldclassmethod = classmethod
from abc-extend 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()

还没有评估此解决方案的性能成本,但到目前为止,它已经为我的目的工作。

最新更新