abc - Classi Base Astratte

Scopo Definisce ed usa classi base per verifiche API nel proprio codice
Versione Python 2.6

Perchè usare le Classi Base Astratte?

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.

Come funziona abc

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

Registrare una Classe Concreta

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

Implementazione Attraverso la Derivazione

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.

Implementazioni Incomplete

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, in 
    print 'Istanza    :', isinstance(IncompleteImplementation(), PluginBase)
TypeError: Can't instantiate abstract class IncompleteImplementation with abstract methods load

Metodi Concreti in abc

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

Proprietà Astratte

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

Tipi Collection

Il modulo collection definisce parecchie classi base astratte in relazione ai tipi contenitore.

Classi contenitore generiche:

  • Container
  • Sized

Classi di Iteratori e Sequenze:

  • Iterable
  • Iterator
  • Sequence
  • MutableSequence

Valori Univoci

  • Hashable
  • Set
  • MutableSet

Mappature:

  • Mapping
  • MutableMapping
  • MappingView
  • KeysView
  • ItemsView
  • ValuesView

Miscellanea:

  • Callable

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:

abc
La documentazione della libreria standard per questo modulo
PEP 3119
Introduzione alle classi base astratte
collections
La documentazione della libreria standard per le collezioni
PEP 3141
Una gerarchia di tipo per i numeri
Wikipedia: Strategy Pattern
Descrizione ed esempi per lo strategy pattern
Plugins and monkeypatching
Presentazione a PyCon 2009 del Dr. André Roberge (Video)