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