Scopo | Definisce ed usa classi base per verifiche API nel proprio codice |
Versione Python | 2.6 |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo abc
Le classi base astratte sono una forma di verifica di interfaccia più stretta dei controlli individuali per particolari metodi come hasattr() . Definendo una classe base astratta si può definire una API comune per un insieme di sottoclassi.
Le classi base astratte sono una forma di verifica di interfaccia più stretta dei controlli individuali per particolari metodi come
hasattr()
. Definendo una classe base astratta si può definire una
API
comune per un insieme di sottoclassi. Questa capacità è specialmente utile in situazioni dove un terzo andrà a fornire una implementazione, ad esempio con plugin ad una applicazione, ma può anche essere d'aiuto quando si lavora in squadra con un grande numero di componenti oppure con una base di codice molto vasta, dove mantenere tutte le classi nella propria testa è allo stesso tempo difficile o non possibile.
abc
funziona marcando i metodi della classe base come astratti, quindi registra le classi concrete come implementazioni della base astratta. Se il proprio codice richiede una
API
particolare, si può usare
issubclass()
oppure
isinstance()
per verificare un oggetto rispetto alla classe base.
Iniziamo con il definire una classe base astratta per rappresentare l'API di un insieme di plugin per salvare e caricare dati.
import abc
class PluginBase(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def load(self, input):
"""Recupera dati dalla sorgente in input e ritorna un oggetto"""
return
@abc.abstractmethod
def save(self, output, data):
"""Salva l'oggetto dati in output."""
return
Ci sono due modi per indicare che una classe concreta ne implementa una astratta: registrare la classe con abc o derivare direttamente da abc
import abc
from abc_base import PluginBase
class RegisteredImplementation(object):
def load(self, input):
return input.read()
def save(self, output, data):
return output.write(data)
PluginBase.register(RegisteredImplementation)
if __name__ == '__main__':
print 'Sottoclasse:', issubclass(RegisteredImplementation, PluginBase)
print 'Istanza :', isinstance(RegisteredImplementation(), PluginBase)
In questo esempio
PluginImplentation
non è derivata da
PluginBase
ma è registrata come implementazione dell'API
PluginBase
$ python abc_register.py Sottoclasse: True Istanza : True
Derivando direttamente dalla base, si può evitare la necessità di registare la classe esplicitamente
import abc
from abc_base import PluginBase
class SubclassImplementation(PluginBase):
def load(self, input):
return input.read()
def save(self, output, data):
return output.write(data)
if __name__ == '__main__':
print 'Sottoclasse:', issubclass(SubclassImplementation, PluginBase)
print 'Istanza :', isinstance(SubclassImplementation(), PluginBase)
In questo caso la normale gestione delle classi di Python viene usata per riconoscere
PluginImplementation
come implementazione della classe astratta
PluginBase
$ python abc_subclass.py Sottoclasse: True Istanza : True
Un effetto collaterale nell'uso della derivazione diretta è che è possibile trovare tutte le implementazioni del proprio plugin interrogando la classe base per ottenere la lista delle classi derivate conosciute derivate da essa (non si tratta di una caratteristica di abc , tutte le classi lo possono fare).
import abc
from abc_base import PluginBase
import abc_subclass
import abc_register
for sc in PluginBase.__subclasses__():
print sc.__name__
Si noti che, sebbene
abc_register
sia importato,
RegisteredImplementation
non è tra la lista di sottoclassi visto che non è in realtà derivata dalla classe base.
$ python abc_find_subclasses.py SubclassImplementation
Il Dr. André Roberge ha descritto l'uso di questa capacità per scoprire i plugin importando tutti i moduli in una directory dinamicamente, quindi cercando nell'elenco di tutte le sottoclassi per trovare le classi di implementazione.
Un altro beneficio del derivare direttamente dalla propria classe base astratta è che la sottoclasse non può essere istanziata a meno che essa implementi pienamente la porzione astratta dell'API. Questo preserva implementazioni parziali dallo scatenare errori inaspettati in fase di esecuzione.
import abc
from abc_base import PluginBase
class IncompleteImplementation(PluginBase):
def save(self, output, data):
return output.write(data)
PluginBase.register(IncompleteImplementation)
if __name__ == '__main__':
print 'Sottoclasse:', issubclass(IncompleteImplementation, PluginBase)
print 'Istanza :', isinstance(IncompleteImplementation(), PluginBase)
$ python abc_incomplete.py Sottoclasse: True Istanza : Traceback (most recent call last): File "abc_incomplete.py", line 16, inprint 'Istanza :', isinstance(IncompleteImplementation(), PluginBase) TypeError: Can't instantiate abstract class IncompleteImplementation with abstract methods load
Sebbene una classe concreta debba provvedere una implementazione di metodi astratti, la classe base astratta può anche fornire una implementazione che può essere chiamata attraverso
super()
. Ciò consente di riutilizzare della logica comune piazzandola nella classe base, ma forzando le derivate a riscrivere il metodo se ha (potenzialmente) della logica proprietaria.
import abc
from cStringIO import StringIO
class ABCWithConcreteImplementation(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def retrieve_values(self, input):
print 'classe base che legge dati'
return input.read()
class ConcreteOverride(ABCWithConcreteImplementation):
def retrieve_values(self, input):
base_data = super(ConcreteOverride, self).retrieve_values(input)
print 'sottoclasse che ordina dati'
response = sorted(base_data.splitlines())
return response
input = StringIO("""riga uno
riga due
riga tre
""")
Visto che
ABCWithConcreteImplementation
è una classe base astratta, non è possibile istanziarla per usarla direttamente. La classe derivata
deve
fornire una riscrittura per
retrieve_values
, ed in questo caso la classe concreta riordina i dati prima di ritornarli
$ python abc_concrete_method.py classe base che legge dati sottoclasse che ordina dati ['riga due', 'riga tre', 'riga uno']
Se la propria specifica
API
comprende attributi, oltre a metodi, si possono richiedere gli attributi nelle classi concrete definendoli tramite
@abstractproperty
.
import abc
class Base(object):
__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def value(self):
return 'Non si dovrebbe mai arrivare qui'
class Implementation(Base):
@property
def value(self):
return 'proprietà concreta'
try:
b = Base()
print 'Base.value:', b.value
except Exception, err:
print 'ERROR:', str(err)
i = Implementation()
print 'Implementation.value:', i.value
La classe
Base
nell'esempio non può essere istanziata visto che ha solo una versione astratta della proprietà
value
$ python abc_abstract_property.py ERRORE: Can't instantiate abstract class Base with abstract methods value Implementation.value: proprietà concreta
Si possono anche definire proprietà astratte per lettura e scrittura
import abc
class Base(object):
__metaclass__ = abc.ABCMeta
def value_getter(self):
return 'Questo non si dovrebbe mai vedere'
def value_setter(self, newvalue):
return
value = abc.abstractproperty(value_getter, value_setter)
class PartialImplementation(Base):
@abc.abstractproperty
def value(self):
return 'Sola lettura'
class Implementation(Base):
_value = 'Valore predefinito'
def value_getter(self):
return self._value
def value_setter(self, newvalue):
self._value = newvalue
value = property(value_getter, value_setter)
try:
b = Base()
print 'Base.value:', b.value
except Exception, err:
print 'ERRORE:', str(err)
try:
p = PartialImplementation()
print 'PartialImplementation.value:', p.value
except Exception, err:
print 'ERRORE:', str(err)
i = Implementation()
print 'Implementation.value:', i.value
i.value = 'Nuovo valore'
print 'Valore modificato:', i.value#if __name__ == '__main__':
Si noti che la proprietà concreta deve essere definita allo stesso modo di quella astratta. Cercando di sovrascrivere una proprietà in lettura/scrittura in
PartialImplementation
con una a sola lettura non funzionerà.
$ python abc_abstract_property_rw.py ERRORE: Can't instantiate abstract class Base with abstract methods value ERRORE: Can't instantiate abstract class PartialImplementation with abstract methods value Implementation.value: Valore predefinito Valore modificato: Nuovo valore
Per usare la sintassi del decoratore con le proprietà di lettura/scrittura astratte, i metodi per ottenere ed impostare il valore dovrebbero essere chiamati allo stesso modo
import abc
class Base(object):
__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def value(self):
return 'Questo non si dovrebbe mai vedere'
@value.setter
def value(self, newvalue):
return
class Implementation(Base):
_value = 'Valore predefinito'
@property
def value(self):
return self._value
@value.setter
def value(self, newvalue):
self._value = newvalue
i = Implementation()
print 'Implementation.value:', i.value
i.value = 'Nuovo valore'
print 'Valore modificato:', i.value
Si noti che entrambi i metodi nelle classi
Base
ed
Implementation
sono chiamati
value()
, sebbene abbiano diverse impronte.
s$ python abc_abstract_property_rw_deco.py Implementation.value: Valore predefinito Valore modificato: Nuovo valore
Il modulo collection definisce parecchie classi base astratte in relazione ai tipi contenitore.
Classi contenitore generiche:
Classi di Iteratori e Sequenze:
Valori Univoci
Mappature:
Miscellanea:
Oltre a servire come esempi dettagliati ed applicabili di classi base astratte, i tipi builtin di Python sono registrati automaticamente a quelle classi quando si importa
collection
. Il che vuol dire che si può usare
isinstance()
in sicurezza per verificare parametri nel proprio codice per assicurarsi che essi supportino l'API che serve. Le classi base possono anche essere usate per definire i propri tipi di collezione, visto che molti di essi forniscono concrete implementazioni della logica interna e necessitano solo di qualche sovrascrittura di metodi. Fare riferimento alla documentazione della libreria standard per quanto riguarda le collezioni per maggiori dettagli
Vedere anche: