Python 2.7 Комбинируйте abc.abstractmethod и classmethod
Как создать декоратор для метода абстрактного класса в Python 2.7?
Да, это похоже на этот вопрос, за исключением того, что я хотел бы объединить abc.abstractmethod
и classmethod
вместо staticmethod
. Кроме того, похоже, что abc.abstractclassmethod
был добавлен в Python 3 (я думаю?), но я использую Google App Engine, поэтому я 'в настоящее время ограничено Python 2.7
Спасибо заранее.
Ответы
Ответ 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()
Я не оценил стоимость исполнения этого решения, но он до сих пор работал для моих целей.