Perché usare le classi di base astratte in Python?


213

Poiché sono abituato ai vecchi modi di scrivere l'anatra in Python, non riesco a capire la necessità di ABC (classi di base astratte). L' aiuto è buono su come usarli.

Ho provato a leggere la logica del PEP , ma è andata oltre la mia testa. Se stavo cercando un contenitore di sequenze mutabili, verificherei __setitem__, o più probabilmente proverei a usarlo ( EAFP ). Non ho trovato un uso reale per il modulo numeri , che utilizza gli ABC, ma questo è il più vicino che devo capire.

Qualcuno può spiegarmi la logica, per favore?

Risposte:


162

Versione breve

Gli ABC offrono un livello più elevato di contratto semantico tra i clienti e le classi implementate.

Versione lunga

C'è un contratto tra una classe e i suoi chiamanti. La classe promette di fare determinate cose e avere determinate proprietà.

Esistono diversi livelli nel contratto.

A un livello molto basso, il contratto potrebbe includere il nome di un metodo o il suo numero di parametri.

In un linguaggio di tipo statico, quel contratto verrebbe effettivamente applicato dal compilatore. In Python, è possibile utilizzare EAFP o digitare introspezione per confermare che l'oggetto sconosciuto soddisfa questo contratto previsto.

Ma ci sono anche promesse semantiche di livello superiore nel contratto.

Ad esempio, se esiste un __str__()metodo, si prevede che restituisca una rappresentazione in formato stringa dell'oggetto. Si potrebbe eliminare tutti i contenuti dell'oggetto, il commit della transazione e sputare una pagina vuota dalla stampante ... ma c'è una comune comprensione di quello che dovrebbe fare, descritto nel manuale Python.

Questo è un caso speciale, in cui il contratto semantico è descritto nel manuale. Cosa dovrebbe print()fare il metodo? Dovrebbe scrivere l'oggetto su una stampante o una linea sullo schermo o qualcos'altro? Dipende: è necessario leggere i commenti per comprendere il contratto completo qui. Un pezzo di codice client che verifica semplicemente che il print()metodo esista ha confermato parte del contratto - che è possibile effettuare una chiamata al metodo, ma non che vi sia un accordo sulla semantica di livello superiore della chiamata.

La definizione di una classe base astratta (ABC) è un modo per produrre un contratto tra gli implementatori di classe e i chiamanti. Non è solo un elenco di nomi di metodi, ma una comprensione condivisa di ciò che tali metodi dovrebbero fare. Se erediti da questa ABC, stai promettendo di seguire tutte le regole descritte nei commenti, inclusa la semantica del print()metodo.

La tipizzazione anatra di Python ha molti vantaggi in termini di flessibilità rispetto alla tipizzazione statica, ma non risolve tutti i problemi. Gli ABC offrono una soluzione intermedia tra la forma libera di Python e la schiavitù e la disciplina di un linguaggio tipicamente statico.


12
Penso che tu abbia un punto lì, ma non posso seguirti. Quindi qual è la differenza, in termini di contratto, tra una classe che implementa __contains__e una classe che eredita collections.Container? Nel tuo esempio, in Python c'era sempre una comprensione condivisa di __str__. L'implementazione __str__fa le stesse promesse ereditate da alcuni ABC e quindi implementate __str__. In entrambi i casi è possibile interrompere il contratto; non ci sono semantiche dimostrabili come quelle che abbiamo nella tipizzazione statica.
Muhammad Alkarouri,

15
collections.Containerè un caso degenerato, che include \_\_contains\_\_solo e significa solo la convenzione predefinita. L'uso di un ABC non aggiunge molto valore da solo, sono d'accordo. Sospetto che sia stato aggiunto per consentire (ad esempio) Setdi ereditare da esso. Quando arrivi Set, improvvisamente appartenere alla ABC ha una notevole semantica. Un oggetto non può appartenere alla collezione due volte. Ciò NON è rilevabile dall'esistenza di metodi.
Pensando in modo strano il

3
Sì, penso che Setsia un esempio migliore di print(). Stavo tentando di trovare un nome di metodo il cui significato fosse ambiguo, e non potevo essere criticato dal solo nome, quindi non potevi essere sicuro che avrebbe fatto la cosa giusta solo con il suo nome e il manuale di Python.
Pensando in modo strano

3
Qualche possibilità di riscrivere la risposta Setcome esempio anziché print? Setha molto senso, @Oddthinking.
Ehtesh Choudhury,

1
Penso che questo articolo lo spieghi molto bene: dbader.org/blog/abstract-base-classes-in-python
szabgab

228

La risposta di Oddthinking non è sbagliata, ma penso che manchi il vero , pratico motivo per cui Python ha l'ABC in un mondo di dattilografia.

I metodi astratti sono accurati, ma secondo me in realtà non riempiono tutti i casi d'uso non già coperti dalla digitazione dell'anatra. Il vero potere delle classi di base astratte sta nel modo in cui consentono di personalizzare il comportamento di isinstanceeissubclass . ( __subclasshook__è fondamentalmente un'API più intuitiva in cima a Python __instancecheck__e agli__subclasscheck__ hook). L'adattamento dei costrutti integrati per lavorare su tipi personalizzati fa molto parte della filosofia di Python.

Il codice sorgente di Python è esemplare. Ecco come collections.Containerviene definito nella libreria standard (al momento della scrittura):

class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Questa definizione di __subclasshook__dice che qualsiasi classe con un __contains__attributo è considerata una sottoclasse di Container, anche se non la subclasse direttamente. Quindi posso scrivere questo:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

In altre parole, se si implementa l'interfaccia giusta, si è una sottoclasse! Gli ABC forniscono un modo formale per definire le interfacce in Python, pur rimanendo fedeli allo spirito della battitura delle anatre. Inoltre, questo funziona in modo da onorare il principio aperto-chiuso .

Il modello a oggetti di Python sembra superficialmente simile a quello di un sistema OO più "tradizionale" (con questo intendo Java *) - abbiamo le tue classi, i tuoi oggetti, i tuoi metodi - ma quando gratti la superficie troverai qualcosa di molto più ricco e più flessibile. Allo stesso modo, la nozione di Python di classi base astratte può essere riconoscibile per uno sviluppatore Java, ma in pratica sono intesi per uno scopo molto diverso.

A volte mi ritrovo a scrivere funzioni polimorfiche che possono agire su un singolo oggetto o una raccolta di oggetti e trovo isinstance(x, collections.Iterable)molto più leggibile di hasattr(x, '__iter__')un try...exceptblocco equivalente . (Se non conoscessi Python, quale di questi tre renderebbe più chiara l'intenzione del codice?)

Detto questo, trovo che raramente ho bisogno di scrivere la mia ABC e in genere scopro la necessità di uno attraverso il refactoring. Se vedo una funzione polimorfica eseguire molti controlli degli attributi o molte funzioni che eseguono gli stessi controlli degli attributi, quell'odore suggerisce l'esistenza di una ABC in attesa di essere estratta.

* senza entrare nel dibattito sul fatto che Java sia un sistema OO "tradizionale" ...


Addendum : anche se una classe base astratta può sovrascrivere il comportamento di isinstancee issubclass, tuttavia non entra nell'MRO della sottoclasse virtuale. Questa è una potenziale trappola per i clienti: non tutti gli oggetti per i quali isinstance(x, MyABC) == Truesono stati definiti i metodi MyABC.

class MyABC(metaclass=abc.ABCMeta):
    def abc_method(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        return True

class C(object):
    pass

# typical client code
c = C()
if isinstance(c, MyABC):  # will be true
    c.abc_method()  # raises AttributeError

Purtroppo questa di quelle trappole "semplicemente non farlo" (di cui Python ne ha relativamente poche!): Evita di definire ABC con __subclasshook__metodi sia a che non astratti. Inoltre, dovresti rendere __subclasshook__coerente la tua definizione con l'insieme di metodi astratti definiti dalla tua ABC.


21
"se implementi la giusta interfaccia, sei una sottoclasse" Grazie mille per quello. Non so se Oddthinking lo abbia mancato, ma sicuramente l'ho fatto. FWIW, isinstance(x, collections.Iterable)è più chiaro per me e conosco Python.
Muhammad Alkarouri,

Post eccellente. Grazie. Penso che l'addendum, "semplicemente non fare quella" trappola, sia un po 'come fare la normale eredità della sottoclasse ma poi avere la Csottoclasse cancellare (o rovinare irreparabilmente) l' abc_method()eredita da MyABC. La differenza principale è che è la superclasse che sta rovinando il contratto di eredità, non la sottoclasse.
Michael Scott Cuthbert,

Non dovresti fare Container.register(ContainAllTheThings)perché l'esempio dato funzioni?
BoZenKhaa,

1
@BoZenKhaa Il codice nella risposta funziona! Provalo! Il significato di __subclasshook__è "qualsiasi classe che soddisfi questo predicato è considerata una sottoclasse ai fini isinstancee ai issubclasscontrolli, indipendentemente dal fatto che sia stata registrata presso la ABC , e indipendentemente dal fatto che si tratti di una sottoclasse diretta ". Come ho detto nella risposta, se si implementa l'interfaccia giusta, si è una sottoclasse!
Benjamin Hodgson

1
È necessario modificare la formattazione dal corsivo in grassetto. "se implementi la giusta interfaccia, sei una sottoclasse!" Spiegazione molto concisa. Grazie!
marti

108

Una caratteristica utile degli ABC è che se non si implementano tutti i metodi (e le proprietà) necessari si ottiene un errore all'istanza, piuttosto che AttributeError, potenzialmente molto più tardi, quando si tenta effettivamente di utilizzare il metodo mancante.

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
class Base(object, metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`


c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

Esempio da https://dbader.org/blog/abstract-base-classes-in-python

Modifica: per includere la sintassi di python3, grazie a @PandasRocks


5
L'esempio e il collegamento sono stati entrambi molto utili. Grazie!
nalyd88,

se la Base è definita in un file separato, è necessario ereditarla come Base.Base o modificare la riga di importazione in "dalla Base di importazione Base"
Ryan Tennill

Va notato che in Python3 la sintassi è leggermente diversa. Vedere questa risposta: stackoverflow.com/questions/28688784/...
PandasRocks

7
Proveniente da uno sfondo C #, questo è il motivo per usare le classi astratte. Stai fornendo funzionalità, ma affermando che la funzionalità richiede un'ulteriore implementazione. Le altre risposte sembrano mancare questo punto.
Josh Noe,

18

Renderà molto più facile determinare se un oggetto supporta un determinato protocollo senza dover verificare la presenza di tutti i metodi nel protocollo o senza innescare un'eccezione nel profondo del territorio "nemico" a causa del mancato supporto.


6

Il metodo astratto si assicura che qualunque metodo tu stia chiamando nella classe genitore deve essere visualizzato nella classe figlio. Di seguito sono riportati i modi noraml per chiamare e utilizzare abstract. Il programma scritto in python3

Modo normale di chiamare

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

'methodone () si chiama'

c.methodtwo()

NotImplementedError

Con il metodo astratto

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError: impossibile creare un'istanza di figlio di classe astratta con due metodi di metodi astratti.

Poiché methodtwo non viene chiamato nella classe figlio, abbiamo ricevuto un errore. L'implementazione corretta è di seguito

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

'methodone () si chiama'


1
Grazie. Non è lo stesso punto sollevato in Cerberos la risposta sopra?
Muhammad Alkarouri,
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.