2012-06-27 10 views
25

Jak utworzyć dekorator dla metody klasy abstrakcyjnej w Pythonie 2.7?Python 2.7 Łączenie abc.abstractmethod i classmethod

Tak, jest to podobne do this question, z wyjątkiem tego, że chciałbym połączyć abc.abstractmethod i classmethod, zamiast staticmethod. Poza tym wygląda na to, że abc.abstractclassmethod to added in Python 3 (I think?), ale używam Google App Engine, więc obecnie ograniczam się do wersji Python 2.7

Z góry dziękuję.

+0

Jeśli nie możesz użyć abc.abstractclassmethod, to w jaki sposób powstaje kwestia łączenia go z innym dekoratorem? – abarnert

+3

Myśl błędnie przeczytano: abc.abstractclassmethod = abc.abstractmethod + classmethod. Python 2.7 ma metodę abstract i classmethod, ale nie jest to klasa abstrakcyjna – jchu

+1

Mam do czynienia z dokładnie podobnym problemem! Dobre pytanie! –

Odpowiedz

24

Oto przykład praca pochodzi z kodem źródłowym w Pythonie 3.3 jest abc moduł:

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 

Oto jak to wygląda podczas pracy:

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

pamiętać, że ostateczna przykład kończy się niepowodzeniem, ponieważ cls() nie tworzy instancji. ABCMeta zapobiega przedwczesnemu utworzeniu klas, które nie zdefiniowały wszystkich wymaganych metod abstrakcyjnych.

Innym sposobem, aby wywołać awarię gdy from_int() abstrakcyjne metody klasy nazywany jest, aby został on wyjątek:

class DemoABC: 

    __metaclass__ = ABCMeta 

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

Konstrukcja ABCMeta czyni żadnego wysiłku, aby zapobiec metody abstrakcyjne od wezwania do nieodpowiedniej klasy, więc od Ciebie zależy, czy wywołasz niepowodzenie, wywołując cls(), jak zwykle robią to metody klasyczne lub podnosząc wartość NotImplementedError. Tak czy inaczej, dostaniesz niezłą, czystą porażkę.

Jest prawdopodobnie chciałoby się napisać deskryptor do przechwytywania bezpośrednie połączenie do abstrakcyjnej metody klasy, ale to byłoby sprzeczne z ogólnym projekcie ABCMeta (który jest o sprawdzanie wymaganych metod przed instancji raczej niż kiedy są wywoływane metody).

+2

Dobry pomysł na sprawdzenie kodu źródłowego Pythona, ale niestety, podobnie jak przy napisaniu 'classmethod', po którym następuje' abc.abstractmethod', nie wymusza to 'abstractmethod'. tj. mogę nadal wywoływać tę metodę. – jchu

+1

@jchu ABCMeta polega na zapobieganiu powstawaniu klas, gdy brakuje abstrakcyjnych metod. Nie ma kodu, który uniemożliwiałby wywoływanie metod abstrakcyjnych na nieodpowiednich klasach. Jeśli chcesz mieć czysty błąd w wywołaniu metody klasy abstrakcyjnej, to niech podniesie * NotImplementedError * lub spróbuje utworzyć instancję za pomocą 'cls()' '. –

+0

Więc jest to typowy wzorzec Python do tworzenia instancji obiektu z metody class? W moim przypadku użycia nie ma sensu tworzenie obiektu z klasy, co może wyjaśniać moje rozłączenie. W efekcie podniosłem NotImplementedError w abstrakcyjnej klasie bazowej, o czym wspomniałeś w swojej alternatywnej opcji. Ale czy w takim przypadku istnieje jakaś różnica w zachowaniu, jeśli używałeś dekoratora abstractclassmethod lub dekoratora class class (poza czystością intencji)? – jchu

2

Niedawno napotkałem ten sam problem. To znaczy, potrzebowałem abstrakcyjnych metod klasowych, ale nie mogłem użyć Pythona 3 z powodu innych ograniczeń projektu. Rozwiązanie, które wymyśliłem, jest następujące.

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) 

Zastosowanie:

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

A oto niektóre testy pyunit które dają bardziej wyczerpujący pokaz.

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

Nie oceniano koszt wykonania tego rozwiązania, ale pracował dla moich celów tej pory.

14

Innym możliwym obejście:

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 

Teraz jeden wciąż nie może instancję od A aż „some_classmethod” jest realizowany, i to działa, jeśli wdrożyć go z classmethod.

+0

To powinna być najlepsza odpowiedź –