Python 2.7 Combine abc.abstractmethod and classmethod

前端 未结 4 1436
太阳男子
太阳男子 2020-12-15 04:19

How do I create a decorator for an abstract class method in Python 2.7?

Yes, this is similar to this question, except I would like to combine abc.abstractmet

相关标签:
4条回答
  • 2020-12-15 04:55

    You could upgrade to Python 3.

    Starting with Python 3.3, it is possible to combine @classmethod and @abstractmethod:

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

    Thanks to @gerrit for pointing this out to me.

    0 讨论(0)
  • 2020-12-15 04:56

    Here's a working example derived from the source code in Python 3.3's abc module:

    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
    

    Here's what it looks like when running:

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

    Note that the final example fails because cls() won't instantiate. ABCMeta prevents premature instantiation of classes that haven't defined all of the required abstract methods.

    Another way to trigger a failure when the from_int() abstract class method is called is to have it raise an exception:

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

    The design ABCMeta makes no effort to prevent any abstract method from being called on an uninstantiated class, so it is up to you to trigger a failure by invoking cls() as classmethods usually do or by raising a NotImplementedError. Either way, you get a nice, clean failure.

    It is probably tempting to write a descriptor to intercept a direct call to an abstract class method, but that would be at odds with the overall design of ABCMeta (which is all about checking for required methods prior to instantiation rather than when methods are called).

    0 讨论(0)
  • 2020-12-15 04:56

    Another possible workaround:

    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
    

    Now, one still can't instantiate from A until 'some_classmethod' is implemented, and it works if you implement it with a classmethod.

    0 讨论(0)
  • 2020-12-15 04:57

    I recently encountered the same problem. That is, I needed abstract classmethods but was unable to use Python 3 because of other project constraints. The solution I came up with is the following.

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

    And here are some pyunit tests which give a more exhaustive demonstration.

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

    I haven't evaluated the performance cost of this solution, but it has worked for my purposes so far.

    0 讨论(0)
提交回复
热议问题