È possibile creare classi astratte in Python?


321

Come posso fare un abstract di classe o metodo in Python?

Ho provato a ridefinire in questo __new__()modo:

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

ma ora se creo una classe Gche eredita in questo Fmodo:

class G(F):
    pass

quindi non posso neanche istanziare G, dal momento che chiama il __new__metodo della sua super classe .

Esiste un modo migliore per definire una classe astratta?


Sì, puoi creare classi astratte in Python con il modulo abc (classi base astratte). Questo sito ti aiuterà a farlo: http://docs.python.org/2/library/abc.html
ORION

Risposte:


554

Utilizzare il abcmodulo per creare classi astratte. Usa il abstractmethoddecoratore per dichiarare un astratto di metodo e dichiarane un astratto di classe usando uno dei tre modi seguenti, a seconda della tua versione di Python.

In Python 3.4 e versioni successive, puoi ereditare da ABC. Nelle versioni precedenti di Python, devi specificare la metaclasse della tua classe come ABCMeta. Specificare la metaclasse ha una sintassi diversa in Python 3 e Python 2. Le tre possibilità sono mostrate di seguito:

# 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

Indipendentemente dal modo in cui usi, non sarai in grado di creare un'istanza di una classe astratta con metodi astratti, ma sarai in grado di creare un'istanza di una sottoclasse che fornisce definizioni concrete di tali metodi:

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

9
cosa fa il @abstractmethod? Perchè ne hai bisogno? Se la classe è già stata definita come astratta, il compilatore / interpretatore non dovrebbe sapere che tutti i metodi provengono dalla classe astratta in questione?
Charlie Parker,

30
@CharlieParker - La @abstractmethodfa in modo che la funzione decorato deve essere ignorata prima della classe può essere istanziata. Dai documenti:A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden.
Nome falso

6
@CharlieParker - Fondamentalmente, ti consente di definire una classe in un modo in cui la sottoclasse deve implementare una specifica suite di metodi per creare un'istanza.
Nome falso

13
C'è un modo per vietare agli utenti di creare Abstract () senza alcun metodo @abstractmethod?
Joe,

2
@ Joe appare è possibile utilizzare @abstractmethodper __init__il metodo così, vedi stackoverflow.com/q/44800659/547270
scrutari

108

Il modo di fare la vecchia scuola (pre PEP 3119 ) per farlo è solo raise NotImplementedErrornella classe astratta quando viene chiamato un metodo astratto.

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): [...]

Questo non ha le stesse belle proprietà dell'uso del abcmodulo. Puoi ancora creare un'istanza della classe base astratta stessa e non troverai l'errore finché non chiami il metodo abstract in fase di esecuzione.

Ma se hai a che fare con un piccolo insieme di classi semplici, forse con solo alcuni metodi astratti, questo approccio è un po 'più semplice che cercare di approfondire la abcdocumentazione.


1
Apprezzo la semplicità e l'efficacia di questo approccio.
mohit6up

Haha, l'ho visto ovunque nel mio corso OMSCS e non avevo idea di cosa fosse :)
mLstudent33

18

Ecco un modo molto semplice senza dover gestire il modulo ABC.

Nel __init__metodo della classe che vuoi essere una classe astratta, puoi controllare il "tipo" di sé. Se il tipo di sé è la classe base, il chiamante sta provando a creare un'istanza della classe base, quindi solleva un'eccezione. Ecco un semplice esempio:

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

Quando eseguito, produce:

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

Ciò dimostra che è possibile creare un'istanza di una sottoclasse che eredita da una classe base, ma non è possibile creare un'istanza direttamente nella classe base.


11

La maggior parte delle risposte precedenti erano corrette, ma ecco la risposta e l'esempio per Python 3.7. Sì, puoi creare una classe e un metodo astratti. Proprio come promemoria a volte una classe dovrebbe definire un metodo che logicamente appartiene a una classe, ma quella classe non può specificare come implementare il metodo. Ad esempio, nelle seguenti classi Genitori e bambini mangiano entrambi, ma l'implementazione sarà diversa per ciascuno perché neonati e genitori mangiano un diverso tipo di cibo e il numero di volte che mangiano è diverso. Quindi, le sottoclassi dei metodi alimentari sovrascrivono 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())

PRODUZIONE:

moms ----------
eat solid food 3 times each day
infants ----------
Milk only 3 times or more each day

Forse import abcè inutile.
Benyamin Jafari,

Possiamo anche scrivere @abstractmethod sulla funzione init ?
variabile

+1 Perché @abstractmethod def eat (self)? Se questa classe è astratta e quindi non è pensata per essere istanziata, perché passi (da solo) a mangiare? Funziona bene senza di essa
VMMF

7

Questo funzionerà in Python 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

4

Come spiegato nelle altre risposte, sì, puoi usare le classi astratte in Python usando il abcmodulo . Qui di seguito un esempio reale utilizzando astratta @classmethod, @propertye @abstractmethod(utilizzando Python 3.6+). Per me è di solito più facile iniziare con esempi che posso facilmente copiare e incollare; Spero che questa risposta sia utile anche per gli altri.

Per prima cosa creiamo una classe base chiamata 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

La nostra Baseclasse avrà sempre a from_dict classmethod, a property prop1(che è di sola lettura) e a property prop2(che può anche essere impostata) oltre a una funzione chiamata do_stuff. Qualunque sia la classe ora costruita sulla base Basedovrà implementare tutte queste per metodi / proprietà. Si noti che affinché un metodo sia astratto, sono necessari due decoratori - classmethode astratto property.

Ora potremmo creare una classe Acome questa:

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

Tutti i metodi / proprietà richiesti sono implementati e possiamo - ovviamente - anche aggiungere funzioni aggiuntive che non fanno parte Base(qui:) i_am_not_abstract.

Ora possiamo fare:

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

a1.prop1
# prints 10

a1.prop2
# prints 'stuff'

Se lo si desidera, non è possibile impostare prop1:

a.prop1 = 100

sarà di ritorno

AttributeError: impossibile impostare l'attributo

Anche il nostro from_dictmetodo funziona bene:

a2.prop1
# prints 20

Se ora abbiamo definito una seconda classe Bcome questa:

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

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

e ho provato a creare un'istanza di un oggetto come questo:

b = B('iwillfail')

avremo un errore

TypeError: impossibile creare un'istanza di classe B astratta con metodi astratti do_stuff, from_dict, prop2

elencando tutte le cose definite in Basecui non abbiamo implementato B.


2

anche questo funziona ed è semplice:

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

2

Puoi anche sfruttare il metodo __new__ a tuo vantaggio. Hai appena dimenticato qualcosa. Il metodo __new__ restituisce sempre il nuovo oggetto, quindi è necessario restituire il nuovo metodo della sua superclasse. Fai come segue.

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)

Quando si utilizza il nuovo metodo, è necessario restituire l'oggetto, non la parola chiave None. Questo è tutto quello che ti sei perso.


2

Trovo la risposta accettata, e tutte le altre strane, dal momento che passano selfa una classe astratta. Una classe astratta non è istanziata, quindi non può avere a self.

Quindi prova questo, funziona.

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

1
Anche la documentazione di abc usa nel loro link di
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

Bello @ Shivam Bharadwaj, l'ho fatto allo stesso modo
Muhammad Irfan

-3

Nel tuo frammento di codice, puoi anche risolverlo fornendo un'implementazione per il __new__metodo nella sottoclasse, allo stesso modo:

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

Ma questo è un trucco e ti sconsiglio, a meno che tu non sappia cosa stai facendo. Per quasi tutti i casi ti consiglio di usare ilabc modulo, che altri prima di me hanno suggerito.

Anche quando si crea un nuovo (di base) di classe, lo rendono sottoclasse object, in questo modo: class MyBaseClass(object):. Non so più se è così significativo, ma aiuta a mantenere la coerenza dello stile sul tuo codice


-4

Solo una rapida aggiunta alla risposta di vecchia scuola di @ TimGilbert ... puoi fare in modo che il metodo init () della tua classe di base astratta generi un'eccezione e ciò gli impedirebbe di essere istanziato, no?

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

6
Ciò impedirà alle sottoclassi di beneficiare di qualsiasi codice init comune che logicamente dovrebbe essere presente nella loro classe base comune.
Arpit Singh,

Giusto. Questo per quanto riguarda la vecchia scuola.
Dave Wade-Stein,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.