Подтвердить что ты не робот

Python 2.7 Комбинируйте abc.abstractmethod и classmethod

Как создать декоратор для метода абстрактного класса в Python 2.7?

Да, это похоже на этот вопрос, за исключением того, что я хотел бы объединить abc.abstractmethod и classmethod вместо staticmethod. Кроме того, похоже, что abc.abstractclassmethod был добавлен в Python 3 (я думаю?), но я использую Google App Engine, поэтому я 'в настоящее время ограничено Python 2.7

Спасибо заранее.

4b9b3361

Ответ 1

Вот рабочий пример, полученный из исходного кода в модуле abc модуля Python 3.3:

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(), как обычно это делают методы класса, или путем создания NotImplementedError. В любом случае, вы получаете хороший, чистый отказ.

Вероятно, соблазн написать дескриптор для перехвата прямого вызова метода абстрактного класса, но это будет противоречить общему дизайну ABCMeta (это все касается проверки необходимых методов до создания экземпляра, а не методов называются).

Ответ 2

Еще одно возможное обходное решение:

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

Теперь все еще невозможно создать экземпляр из A до тех пор, пока не будет реализован "some_classmethod", и он будет работать, если вы реализуете его с помощью метода class.

Ответ 3

Недавно я столкнулся с той же проблемой. То есть мне нужны абстрактные методы класса, но я не смог использовать Python 3 из-за других ограничений проекта. Решение, которое я придумал, следующее.

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)

Использование:

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

И вот несколько тестов пинитов, которые дают более исчерпывающую демонстрацию.

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

Я не оценил стоимость исполнения этого решения, но он до сих пор работал для моих целей.