abc - Classi Base Astratte
Scopo: Definisce e usa classi base per verifiche di interfaccia
Perchè usare le Classi Base Astratte?
Le classi base astratte sono una forma di verifica di interfaccia più stretta delle verifiche individuali con hasattr() per particolari metodi. Definendo una classe base astratta si può definire una API comune per un insieme di sottoclassi. Questa capacità è specialmente utile in situazioni dove qualcuno che non ha dimestichezza con il codice di una applicazione andrà a fornire estensioni plug-in, 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 traccia di tutte le classi è difficile o non possibile.
Come Funziona abc
abc funziona marcando i metodi della classe base come astratti, quindi registrando le classi concrete come implementazioni della base astratta. Se una applicazione o libreria richiede una API particolare, si può usare issubclass()
oppure isinstance()
per verificare un oggetto rispetto alla classe astratta.
Per iniziare si definisce una classe base astratta per rappresentare l'API di un insieme di plug-in per salvare e caricare dati. Si imposta la meta-classe per nuova classe base come ABCMeta
, utilizzando i decoratori per stabilire l'API pubblica per la classe. Gli esempi seguenti utilizzano abc_base.py
che contiene:
# abc_base.py
import abc
class PluginBase(metaclass=abc.ABCMeta):
@abc.abstractmethod
def load(self, input):
"""Recupera dati dalla sorgente in input e ritorna un oggetto"""
@abc.abstractmethod
def save(self, output, data):
"""Salva l'oggetto dati in output."""
Registrare una Classe Concreta
Ci sono due modi per indicare che una classe concreta implementa una API astratta: registrare esplicitamente la classe o creare una nuova sottoclasse direttamente dalla classe base astratta. Si utilizza il metodo di classe register()
come decoratore su di una classe concreta per aggiungerlo esplicitamente quando la classe fornisce l'API richiesta, ma non è parte dell'albero di ereditarietà della classe astratta base.
# abc_register.py
import abc
from abc_base import PluginBase
class LocalBaseClass:
pass
@PluginBase.register
class RegisteredImplementation(LocalBaseClass):
def load(self, input):
return input.read()
def save(self, output, data):
return output.write(data)
if __name__ == '__main__':
print('Subclass:', issubclass(RegisteredImplementation,
PluginBase))
print('Instance:', isinstance(RegisteredImplementation(),
PluginBase))
In questo esempio RegisteredImplentation
è derivata da LocalBaseClass
ma è registrata come implementazione dell'API PluginBase
in modo che issubclass()
ed isinstance()
la trattino come se fosse derivata da PluginBase
.
$ python3 abc_register.py Subclass: True Instance: True
Implementazione Attraverso la Derivazione
Derivando direttamente dalla base, si può evitare la necessità di registrare la classe esplicitamente.
# abc_subclass.py
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(RegisteredImplementation,
PluginBase))
print('Istanza:', isinstance(RegisteredImplementation(),
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 Traceback (most recent call last): File "abc_subclass.py", line 17, in <module> print('Sottoclasse:', issubclass(RegisteredImplementation, NameError: name 'RegisteredImplementation' is not defined
Un effetto collaterale nell'uso della derivazione diretta è che è possibile trovare tutte le implementazioni di un plug-in 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).
# abc_find_subclasses.py
import abc
from abc_base import PluginBase
import abc_subclass
import abc_register
for sc in PluginBase.__subclasses__():
print sc.__name__
Sebbene abc_register
sia importato, RegisteredImplementation
non è nella lista di sottoclassi visto che non è in realtà derivata dalla classe base.
$ python3 abc_find_subclasses.py File "abc_find_subclasses.py", line 9 print sc.__name__ ^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(sc.__name__)?
Un Aiuto per la Classe Base
Dimenticare di impostare propriamente la meta-classe ha come conseguenza che le implementazioni concrete non avranno le proprie API forzate. Per facilitare la corretta impostazione della classe astratta viene fornita una classe base che imposta la meta-classe.
# abc_abc_base.py
import abc
class PluginBase(abc.ABC):
@abc.abstractmethod
def load(self, input):
"""Ottiene i dati dalla sorgente in input
e ritorna un oggetto
"""
@abc.abstractmethod
def save(self, output, data):
"""Salva l'oggetto dati verso l'output."""
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))
Per creare una nuova classe astratta, ereditare semplicemente da ABC.
$ python3 abc_abc_base.py Sottoclasse: True Istanza: True
Implementazioni Incomplete
Un altro beneficio del derivare direttamente dalla classe base astratta è che la sottoclasse non può essere istanziata a meno che essa implementi pienamente la porzione astratta dell'API.
# abc_incomplete.py
import abc
from abc_base import PluginBase
@PluginBase.register
class IncompleteImplementation(PluginBase):
def save(self, output, data):
return output.write(data)
if __name__ == '__main__':
print('Sottoclasse:', issubclass(IncompleteImplementation,
PluginBase))
print('Istanza:', isinstance(IncompleteImplementation(),
PluginBase))
Questo preserva implementazioni parziali dallo scatenare errori inaspettati in fase di esecuzione.
$ python3 abc_incomplete.py Sottoclasse: True Traceback (most recent call last): File "abc_incomplete.py", line 16, in <module> print('Istanza:', isinstance(IncompleteImplementation(), TypeError: Can't instantiate abstract class IncompleteImplementation with abstract methods load
Metodi Concreti in ABC
Sebbene una classe concreta debba provvedere una implementazione per tutti i metodi astratti, la classe base astratta può anche fornire implementazioni che possono essere chiamate attraverso super()
. Ciò consente di riutilizzare della logica comune piazzandola nella classe base, ma forzando le derivate a riscrivere il metodo se lo stesso ha (potenzialmente) della logica personalizzata.
# abc_concrete_method.py
import abc
import io
class ABCWithConcreteImplementation(abc.ABC):
@abc.abstractmethod
def retrieve_values(self, input):
print('classe base per leggere 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 = io.StringIO("""riga uno
riga due
riga tre
""")
reader = ConcreteOverride()
print(reader.retrieve_values(input))
print()
Visto che ABCWithConcreteImplementation
è una classe base astratta, non è possibile istanziarla per usarla direttamente. Le classi derivate devono fornire un overrride per retrieve_values
, e in questo caso la classe concreta riordina i dati prima di ritornarli.
$ python3 abc_concrete_method.py classe base per leggere dati sottoclasse che ordina dati ['riga due', 'riga tre', 'riga uno']
Proprietà Astratte
Se una specifica di API comprende attributi in aggiunta a metodi, si possono richiedere gli attributi nelle classi concrete combinando abstractmethod()
con property()
.
# abc_abstractproperty.py
import abc
class Base(abc.ABC):
@property
@abc.abstractmethod
def value(self):
return 'Non si dovrebbe mai arrivare qui'
@property
@abc.abstractmethod
def constant(self):
return 'Non si dovrebbe mai arrivare qui'
class Implementation(Base):
@property
def value(self):
return 'proprietà concreta'
constant = 'impostata da un attributo di classe'
try:
b = Base()
print('Base.value:', b.value)
except Exception as err:
print('ERRORE:', str(err))
i = Implementation()
print('Implementation.value :', i.value)
print('Implementation.constant:', i.constant)
La classe Base
nell'esempio non può essere istanziata visto che ha solo una versione astratta dei metodi getter per value
e costant
. Alla proprietà value
viene dato un getter concreto in Implementation
e constant
viene definita attraverso un attributo di classe.
$ python3 abc_abstractproperty.py ERRORE: Can't instantiate abstract class Base with abstract methods constant, value Implementation.value : proprietà concreta Implementation.constant: impostata da un attributo di classe
Si possono anche definire proprietà astratte per lettura e scrittura.
# abc_abstractproperty_rw.py
import abc
class Base(abc.ABC):
@property
@abc.abstractmethod
def value(self):
return 'Non si dovrebbe mai arrivare qui'
@value.setter
@abc.abstractmethod
def value(self, newvalue):
return
class PartialImplementation(Base):
@property
def value(self):
return 'Sola lettura'
class Implementation(Base):
_value = 'Valore predefinito'
@property
def value(self):
return self._value
@value.setter
def value(self, newvalue):
self._value = newvalue
try:
b = Base()
print('Base.value:', b.value)
except Exception as err:
print('ERRORE:', str(err))
try:
p = PartialImplementation()
print('PartialImplementation.value:', p.value)
p.value = 'Alterazione'
print('PartialImplementation.value:', p.value)
except Exception as err:
print('ERRORE:', str(err))
i = Implementation()
print('Implementation.value:', i.value)
i.value = 'Nuovo valore'
print('Valore modificato:', i.value)
La proprietà concreta deve essere definita allo stesso modo di quella astratta. L'override di una proprietà in lettura/scrittura in PartialImplementation
con una a sola lettura lascia la stessa a sola lettura.
$ python3 abc_abstractproperty_rw.py ERRORE: Can't instantiate abstract class Base with abstract methods value PartialImplementation.value: Sola lettura ERRORE: can't set attribute 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 e impostare il valore dovrebbero essere chiamati allo stesso modo.
Classi Astratte e Metodi Statici
Anche i metodi e le classi statiche possono essere marcati come astratti.
# abc_class_static.py
import abc
class Base(abc.ABC):
@classmethod
@abc.abstractmethod
def factory(cls, *args):
return cls()
@staticmethod
@abc.abstractmethod
def const_behavior():
return 'Non si dovrebbe mai arrivare qui'
class Implementation(Base):
def do_something(self):
pass
@classmethod
def factory(cls, *args):
obj = cls(*args)
obj.do_something()
return obj
@staticmethod
def const_behavior():
return 'Il comportamento statico differisce'
try:
o = Base.factory()
print('Base.value:', o.const_behavior())
except Exception as err:
print('ERRORE:', str(err))
i = Implementation.factory()
print('Implementation.const_behavior :', i.const_behavior())
Sebbene il metodo di classe sia chiamato sulla classe piuttosto che sulla istanza, impedisce comunque alla classe di venire istanziata se non è definito.
$ python3 abc_class_static.py ERRORE: Can't instantiate abstract class Base with abstract methods const_behavior, factory Implementation.const_behavior : Il comportamento statico differisce
Vedere anche:
- abc
- La documentazione della libreria standard per questo modulo
- PEP 3119
- Introduzione alle classi base astratte
- collections
- Il modulo collections include classi base astratte per parecchi tipi collezione.
- PEP 3141
- Una gerarchia di tipo per i numeri
- Wikipedia: Strategy Pattern
- Descrizione ed esempi per lo strategy pattern, un comune modello plug-in di implementazione.
- Dynamic Code Patterns: Extending Your Applications With Plugins
- Presentazione a PyCon 2013 di Doug Hellmann
- Note di portabilità
- Note di portabilità per abc