Czy w Pythonie można tworzyć klasy abstrakcyjne?

321

Jak mogę zrobić streszczenie klasy lub metody w Pythonie?

Próbowałem przedefiniować __new__()tak:

class F:
    def __new__(cls):
        raise Exception("Unable to create an instance of abstract class %s" %cls)

ale teraz, jeśli utworzę klasę, Gktóra odziedziczy po niej F:

class G(F):
    pass

nie mogę też utworzyć instancji G, ponieważ wywołuje __new__metodę superklasy.

Czy istnieje lepszy sposób zdefiniowania klasy abstrakcyjnej?

Martin Thoma
źródło
Tak, możesz tworzyć klasy abstrakcyjne w Pythonie za pomocą modułu abc (abstrakcyjne klasy podstawowe). Ta strona Ci w tym pomoże: http://docs.python.org/2/library/abc.html
ORION

Odpowiedzi:

553

Użyj abcmodułu, aby utworzyć klasy abstrakcyjne. Użyj abstractmethoddekoratora, aby zadeklarować streszczenie metody, i zadeklaruj streszczenie klasy na jeden z trzech sposobów, w zależności od wersji Pythona.

W Pythonie 3.4 i nowszych możesz dziedziczyć po ABC. We wcześniejszych wersjach Pythona musisz określić metaklasę swojej klasy jako ABCMeta. Określanie metaklasy ma inną składnię w Pythonie 3 i Pythonie 2. Trzy możliwości pokazano poniżej:

# Python 3.4+
from abc import ABC, abstractmethod
class Abstract(ABC):
    @abstractmethod
    def foo(self):
        pass
# Python 3.0+
from abc import ABCMeta, abstractmethod
class Abstract(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass
# Python 2
from abc import ABCMeta, abstractmethod
class Abstract:
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

Niezależnie od tego, jakiego użyjesz, nie będziesz w stanie utworzyć instancji klasy abstrakcyjnej, która ma metody abstrakcyjne, ale będziesz w stanie utworzyć instancję podklasy, która zapewnia konkretne definicje tych metod:

>>> Abstract()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Abstract with abstract methods foo
>>> class StillAbstract(Abstract):
...     pass
... 
>>> StillAbstract()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class StillAbstract with abstract methods foo
>>> class Concrete(Abstract):
...     def foo(self):
...         print('Hello, World')
... 
>>> Concrete()
<__main__.Concrete object at 0x7fc935d28898>
alexvassel
źródło
9
co robi @abstractmethod? A po co ci to? Jeśli klasa jest już ustanowiona jako abstrakcyjna, czy kompilator / interpretator nie powinien wiedzieć, że wszystkie metody pochodzą z danej klasy abstrakcyjnej?
Charlie Parker
29
@CharlieParker - @abstractmethodsprawia, że ​​funkcja dekorowana musi zostać zastąpiona przed utworzeniem instancji klasy. Z dokumentów:A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden.
Fałszywe imię
6
@CharlieParker - Zasadniczo pozwala zdefiniować klasę w sposób, w którym podklasa musi zaimplementować określony zestaw metod, aby w ogóle utworzyć instancję.
Fałszywe imię
13
Czy istnieje sposób, aby zabronić użytkownikom tworzenia Abstract () bez żadnych metod @abstractmethod?
Joe
2
@Joe wydaje można użyć @abstractmethoddo __init__sposobu jak dobrze zobaczyć stackoverflow.com/q/44800659/547270
scrutari
108

Starym sposobem (wcześniejszym niż PEP 3119 ) jest to po prostu raise NotImplementedErrorw klasie abstrakcyjnej, gdy wywoływana jest metoda abstrakcyjna.

class Abstract(object):
    def foo(self):
        raise NotImplementedError('subclasses must override foo()!')

class Derived(Abstract):
    def foo(self):
        print 'Hooray!'

>>> d = Derived()
>>> d.foo()
Hooray!
>>> a = Abstract()
>>> a.foo()
Traceback (most recent call last): [...]

To nie ma tych samych dobrych właściwości, co przy użyciu abcmodułu. Nadal możesz utworzyć instancję samej abstrakcyjnej klasy bazowej i nie znajdziesz swojego błędu, dopóki nie wywołasz metody abstrakcyjnej w czasie wykonywania.

Ale jeśli masz do czynienia z niewielkim zestawem prostych klas, być może za pomocą kilku abstrakcyjnych metod, takie podejście jest nieco łatwiejsze niż próba przebicia się przez abcdokumentację.

Tim Gilbert
źródło
1
Doceń prostotę i skuteczność tego podejścia.
mohit6up
Haha, widziałem to wszędzie na moim kursie OMSCS i nie miałem pojęcia, co to było :)
mLstudent33
18

Oto bardzo prosty sposób bez konieczności zajmowania się modułem ABC.

W __init__metodzie klasy, która ma być klasą abstrakcyjną, można sprawdzić „typ” jaźni. Jeśli typem self jest klasa podstawowa, wówczas osoba dzwoniąca próbuje utworzyć instancję klasy podstawowej, więc zgłosz wyjątek. Oto prosty przykład:

class Base():
    def __init__(self):
        if type(self) is Base:
            raise Exception('Base is an abstract class and cannot be instantiated directly')
        # Any initialization code
        print('In the __init__  method of the Base class')

class Sub(Base):
    def __init__(self):
        print('In the __init__ method of the Sub class before calling __init__ of the Base class')
        super().__init__()
        print('In the __init__ method of the Sub class after calling __init__ of the Base class')

subObj = Sub()
baseObj = Base()

Po uruchomieniu tworzy:

In the __init__ method of the Sub class before calling __init__ of the Base class
In the __init__  method of the Base class
In the __init__ method of the Sub class after calling __init__ of the Base class
Traceback (most recent call last):
  File "/Users/irvkalb/Desktop/Demo files/Abstract.py", line 16, in <module>
    baseObj = Base()
  File "/Users/irvkalb/Desktop/Demo files/Abstract.py", line 4, in __init__
    raise Exception('Base is an abstract class and cannot be instantiated directly')
Exception: Base is an abstract class and cannot be instantiated directly

To pokazuje, że można utworzyć instancję podklasy, która dziedziczy z klasy podstawowej, ale nie można bezpośrednio utworzyć instancji klasy podstawowej.

Irv Kalb
źródło
11

Większość poprzednich odpowiedzi była poprawna, ale oto odpowiedź i przykład dla Pythona 3.7. Tak, możesz stworzyć abstrakcyjną klasę i metodę. Dla przypomnienia czasami klasa powinna zdefiniować metodę, która logicznie należy do klasy, ale klasa ta nie może określić, w jaki sposób zaimplementować metodę. Na przykład w poniższych zajęciach z rodzicami i dziećmi jedzą oboje, ale ich realizacja będzie różna dla każdego, ponieważ niemowlęta i rodzice jedzą inny rodzaj jedzenia, a liczba posiłków jest inna. Tak więc, podklasy metod jeść zastępują AbstractClass.eat.

from abc import ABC, abstractmethod

class AbstractClass(ABC):

    def __init__(self, value):
        self.value = value
        super().__init__()

    @abstractmethod
    def eat(self):
        pass

class Parents(AbstractClass):
    def eat(self):
        return "eat solid food "+ str(self.value) + " times each day"

class Babies(AbstractClass):
    def eat(self):
        return "Milk only "+ str(self.value) + " times or more each day"

food = 3    
mom = Parents(food)
print("moms ----------")
print(mom.eat())

infant = Babies(food)
print("infants ----------")
print(infant.eat())

WYNIK:

moms ----------
eat solid food 3 times each day
infants ----------
Milk only 3 times or more each day
grepit
źródło
Być może import abcjest bezużyteczny.
Benyamin Jafari,
Czy możemy również napisać @abstractmethod na funkcji init ?
zmienna
+1 Dlaczego @abstractmethod def def eat (self)? Jeśli ta klasa jest abstrakcyjna, a zatem nie ma na celu jej tworzenia, dlaczego podajesz (siebie) jeść? Bez niego działa dobrze
VMMF
7

Ten będzie działał w Pythonie 3

from abc import ABCMeta, abstractmethod

class Abstract(metaclass=ABCMeta):

    @abstractmethod
    def foo(self):
        pass

Abstract()
>>> TypeError: Can not instantiate abstract class Abstract with abstract methods foo
Rajkumar SR
źródło
4

Jak wyjaśniono w innych odpowiedziach, tak, możesz używać klas abstrakcyjnych w Pythonie za pomocą abcmodułu . Poniżej podam rzeczywisty przykład z wykorzystaniem streszczenia @classmethod, @propertyoraz@abstractmethod (przy użyciu Python 3.6+). Dla mnie zwykle łatwiej jest zacząć od przykładów, które mogę łatwo skopiować i wkleić; Mam nadzieję, że ta odpowiedź będzie przydatna także dla innych.

Utwórzmy najpierw klasę podstawową o nazwie Base:

from abc import ABC, abstractmethod

class Base(ABC):

    @classmethod
    @abstractmethod
    def from_dict(cls, d):
        pass

    @property
    @abstractmethod
    def prop1(self):
        pass

    @property
    @abstractmethod
    def prop2(self):
        pass

    @prop2.setter
    @abstractmethod
    def prop2(self, val):
        pass

    @abstractmethod
    def do_stuff(self):
        pass

Nasza Baseklasa zawsze będzie miała a from_dict classmethod, a property prop1(która jest tylko do odczytu) i a property prop2(którą można również ustawić), a także wywoływaną funkcję do_stuff. Niezależnie od tego, na jakiej podstawie zbudowana jest teraz klasa, Basetrzeba będzie zaimplementować wszystkie te metody / właściwości. Pamiętaj, że aby metoda była abstrakcyjna, wymagane są dwa elementy dekoracyjne - classmethodi abstrakcyjneproperty .

Teraz możemy stworzyć taką klasę A:

class A(Base):
    def __init__(self, name, val1, val2):
        self.name = name
        self.__val1 = val1
        self._val2 = val2

    @classmethod
    def from_dict(cls, d):
        name = d['name']
        val1 = d['val1']
        val2 = d['val2']

        return cls(name, val1, val2)

    @property
    def prop1(self):
        return self.__val1

    @property
    def prop2(self):
        return self._val2

    @prop2.setter
    def prop2(self, value):
        self._val2 = value

    def do_stuff(self):
        print('juhu!')

    def i_am_not_abstract(self):
        print('I can be customized')

Wszystkie wymagane metody / właściwości są zaimplementowane i możemy - oczywiście - również dodać dodatkowe funkcje, które nie są częścią Base(tutaj i_am_not_abstract:).

Teraz możemy zrobić:

a1 = A('dummy', 10, 'stuff')
a2 = A.from_dict({'name': 'from_d', 'val1': 20, 'val2': 'stuff'})

a1.prop1
# prints 10

a1.prop2
# prints 'stuff'

Zgodnie z życzeniem nie możemy ustawić prop1:

a.prop1 = 100

wróci

AttributeError: nie można ustawić atrybutu

Również nasza from_dictmetoda działa dobrze:

a2.prop1
# prints 20

Gdybyśmy teraz zdefiniowali taką drugą klasę B:

class B(Base):
    def __init__(self, name):
        self.name = name

    @property
    def prop1(self):
        return self.name

i próbował utworzyć instancję takiego obiektu:

b = B('iwillfail')

dostaniemy błąd

TypeError: Nie można utworzyć instancji klasy abstrakcyjnej B za pomocą metod abstrakcyjnych do_stuff, from_dict, prop2

wymieniając wszystkie rzeczy, w Basektórych nie wdrożyliśmy B.

Cleb
źródło
2

to również działa i jest proste:

class A_abstract(object):

    def __init__(self):
        # quite simple, old-school way.
        if self.__class__.__name__ == "A_abstract": 
            raise NotImplementedError("You can't instantiate this abstract class. Derive it, please.")

class B(A_abstract):

        pass

b = B()

# here an exception is raised:
a = A_abstract()
Giovanni Angeli
źródło
2

Możesz także wykorzystać metodę __new__ na swoją korzyść. Właśnie o czymś zapomniałeś. Metoda __new__ zawsze zwraca nowy obiekt, dlatego musisz zwrócić nową metodę jego nadklasy. Wykonaj następujące czynności.

class F:
    def __new__(cls):
        if cls is F:
            raise TypeError("Cannot create an instance of abstract class '{}'".format(cls.__name__))
        return super().__new__(cls)

Korzystając z nowej metody, musisz zwrócić obiekt, a nie słowo kluczowe Brak. To wszystko, za czym tęskniłeś.

Michał
źródło
2

Uważam, że przyjęta odpowiedź, a wszystkie inne dziwne, ponieważ przechodzą selfdo klasy abstrakcyjnej. Klasa abstrakcyjna nie jest tworzona, więc nie może mieć klasyself .

Więc spróbuj tego, to działa.

from abc import ABCMeta, abstractmethod


class Abstract(metaclass=ABCMeta):
    @staticmethod
    @abstractmethod
    def foo():
        """An abstract method. No need to write pass"""


class Derived(Abstract):
    def foo(self):
        print('Hooray!')


FOO = Derived()
FOO.foo()
Sean Bradley
źródło
1
Nawet dokumentacja abc używa self w swoim linku do
Igor Smirnov
2
 from abc import ABCMeta, abstractmethod

 #Abstract class and abstract method declaration
 class Jungle(metaclass=ABCMeta):
     #constructor with default values
     def __init__(self, name="Unknown"):
     self.visitorName = name

     def welcomeMessage(self):
         print("Hello %s , Welcome to the Jungle" % self.visitorName)

     # abstract method is compulsory to defined in child-class
     @abstractmethod
     def scarySound(self):
         pass
Shivam Bharadwaj
źródło
Miło @Shivam Bharadwaj, zrobiłem to samo
Muhammad Irfan
-3

We fragmencie kodu można również rozwiązać ten problem, podając implementację __new__metody w podklasie, podobnie:

def G(F):
    def __new__(cls):
        # do something here

Ale to jest hack i odradzam ci to, chyba że wiesz, co robisz. W prawie wszystkich przypadkach radzę korzystać z abcmodułu, który inni wcześniej sugerowali.

Również podczas tworzenia nowej klasy (baza), sprawiają, że podklasy object, tak: class MyBaseClass(object):. Nie wiem, czy jest to już tak znaczące, ale pomaga zachować spójność stylu w kodzie

NlightNFotis
źródło
-4

Szybki dodatek do old-schoolowej odpowiedzi @ TimGilbert ... możesz sprawić, że metoda init () abstrakcyjnej klasy podstawowej będzie generować wyjątek, co uniemożliwiłoby jej utworzenie, nie?

>>> class Abstract(object):
...     def __init__(self):
...         raise NotImplementedError("You can't instantiate this class!")
...
>>> a = Abstract()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
NotImplementedError: You can't instantiate this class! 
Dave Wade-Stein
źródło
6
Zapobiegnie to korzystaniu z podklas przez jakikolwiek wspólny kod init, który logicznie powinien występować we wspólnej klasie bazowej.
Arpit Singh
Słusznie. Tyle o starej szkole.
Dave Wade-Stein,