abc — Clases base abstractas

Propósito:Define y usa clases base abstractas para la verificación de la interfaz.

¿Por qué usar clases base abstractas?

Las clases base abstractas son una forma de comprobación de interfaz más estricta que las comprobaciones individuales hasattr() para métodos particulares. Al definir una clase base abstracta, se puede establecer una interfaz de programación común para un conjunto de subclases. Esta capacidad es especialmente útil en situaciones en las que alguien menos familiarizado con el origen de una aplicación proporcionará extensiones de complementos, pero también puede ayudar cuando se trabaja en un equipo grande o con una base de código grande donde se realiza un seguimiento de todas las clases al mismo tiempo son difíciles o imposibles.

Cómo funcionan las CBAs

abc funciona marcando los métodos de la clase base como abstractos, y luego registrando clases concretas como implementaciones de la base abstracta. Si una aplicación o biblioteca requiere una interfaz de programación particular, issubclass() o isinstance() se pueden usar para verificar un objeto contra la clase abstracta.

Para comenzar, define una clase base abstracta para representar la interfaz de programación de un conjunto de complementos para guardar y cargar datos. Establece la metaclase para la nueva clase base en ABCMeta, y usa decoradores para establecer la interfaz de programación pública para la clase. Los siguientes ejemplos usan abc_base.py.

abc_base.py
import abc


class PluginBase(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def load(self, input):
        """Retrieve data from the input source
        and return an object.
        """

    @abc.abstractmethod
    def save(self, output, data):
        """Save the data object to the output."""

Registrar una clase de concreta

Hay dos formas de indicar que una clase concreta implementa una interfaz de programación abstracta: registrar explícitamente la clase o crear una nueva subclase directamente desde la base abstracta. Usa el método de clase register() como decorador en una clase concreta para agregarla explícitamente cuando la clase proporciona la interfaz de programación requerida, pero no es parte del árbol de herencia de la clase base abstracta.

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))

En este ejemplo, la RegisteredImplementation se deriva de LocalBaseClass, pero está registrada como implementadora de la interfaz de programación PluginBase, por lo que issubclass() e isinstance() la tratan como si se deriva de PluginBase.

$ python3 abc_register.py

Subclass: True
Instance: True

Implementación a través de subclases

Subclasificar directamente desde la base evita la necesidad de registrar la clase explícitamente.

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('Subclass:', issubclass(SubclassImplementation,
                                  PluginBase))
    print('Instance:', isinstance(SubclassImplementation(),
                                  PluginBase))

En este caso, las características normales de administración de la clase Python se utilizan para reconocer SubclassImplementation como la implementación de la PluginBase abstracta.

$ python3 abc_subclass.py

Subclass: True
Instance: True

Un efecto secundario del uso de subclases directas es que es posible encontrar todas las implementaciones de un plug-in solicitando a la clase base la lista de clases conocidas derivadas de ella (esta no es una característica abc, todas las clases puede hacer esto).

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__)

Aunque se importa abc_register(), RegisteredImplementation no se encuentra entre la lista de subclases porque en realidad no se deriva de la base.

$ python3 abc_find_subclasses.py

SubclassImplementation

Clase base auxiliar

Olvidar establecer la metaclase correctamente significa que las implementaciones concretas no tienen sus interfaces de programación forzadas. Para facilitar la configuración adecuada de la clase abstracta, se proporciona una clase base que establece la metaclase automáticamente.

abc_abc_base.py
import abc


class PluginBase(abc.ABC):

    @abc.abstractmethod
    def load(self, input):
        """Retrieve data from the input source
        and return an object.
        """

    @abc.abstractmethod
    def save(self, output, data):
        """Save the data object to the output."""


class SubclassImplementation(PluginBase):

    def load(self, input):
        return input.read()

    def save(self, output, data):
        return output.write(data)


if __name__ == '__main__':
    print('Subclass:', issubclass(SubclassImplementation,
                                  PluginBase))
    print('Instance:', isinstance(SubclassImplementation(),
                                  PluginBase))

Para crear una nueva clase abstracta, simplemente hereda de ABC.

$ python3 abc_abc_base.py

Subclass: True
Instance: True

Implementaciones incompletas

Otro beneficio de subclasificar directamente desde la clase base abstracta es que la subclase no se puede instanciar a menos que implemente completamente la parte abstracta de la interfaz de programación.

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('Subclass:', issubclass(IncompleteImplementation,
                                  PluginBase))
    print('Instance:', isinstance(IncompleteImplementation(),
                                  PluginBase))

Esto evita que las implementaciones incompletas desencadenen errores inesperados en tiempo de ejecución.

$ python3 abc_incomplete.py

Subclass: True
Traceback (most recent call last):
  File "abc_incomplete.py", line 24, in <module>
    print('Instance:', isinstance(IncompleteImplementation(),
TypeError: Can't instantiate abstract class
IncompleteImplementation with abstract methods load

Métodos concretos en CBAs

Aunque una clase concreta debe proporcionar implementaciones de todos los métodos abstractos, la clase base abstracta también puede proporcionar implementaciones que se pueden invocar a través de super(). Esto permite que la lógica común se reutilice colocándola en la clase base, pero obliga a las subclases a proporcionar un método de anulación con una lógica (probablemente) personalizada.

abc_concrete_method.py
import abc
import io


class ABCWithConcreteImplementation(abc.ABC):

    @abc.abstractmethod
    def retrieve_values(self, input):
        print('base class reading data')
        return input.read()


class ConcreteOverride(ABCWithConcreteImplementation):

    def retrieve_values(self, input):
        base_data = super(ConcreteOverride,
                          self).retrieve_values(input)
        print('subclass sorting data')
        response = sorted(base_data.splitlines())
        return response


input = io.StringIO("""line one
line two
line three
""")

reader = ConcreteOverride()
print(reader.retrieve_values(input))
print()

Dado que ABCWithConcreteImplementation() es una clase base abstracta, no es posible crear una instancia para usarla directamente. Las subclases deben proporcionar una anulación para retrieve_values(), y en este caso la clase concreta clasifica los datos antes de devolverlos.

$ python3 abc_concrete_method.py

base class reading data
subclass sorting data
['line one', 'line three', 'line two']

Propiedades abstractas

Si una especificación de interfaz de programación incluye atributos además de métodos, puedes requerir los atributos en clases concretas combinando abstractmethod() con property().

abc_abstractproperty.py
import abc


class Base(abc.ABC):

    @property
    @abc.abstractmethod
    def value(self):
        return 'Should never reach here'

    @property
    @abc.abstractmethod
    def constant(self):
        return 'Should never reach here'


class Implementation(Base):

    @property
    def value(self):
        return 'concrete property'

    constant = 'set by a class attribute'


try:
    b = Base()
    print('Base.value:', b.value)
except Exception as err:
    print('ERROR:', str(err))

i = Implementation()
print('Implementation.value   :', i.value)
print('Implementation.constant:', i.constant)

La clase Base en el ejemplo no se puede instanciar porque solo tiene una versión abstracta de los métodos de obtención de propiedades para value y constant. La propiedad value recibe un getter concreto en Implementation y constant se define usando un atributo de clase.

$ python3 abc_abstractproperty.py

ERROR: Can't instantiate abstract class Base with abstract
methods constant, value
Implementation.value   : concrete property
Implementation.constant: set by a class attribute

Las propiedades abstractas de lectura y escritura también se pueden definir.

abc_abstractproperty_rw.py
import abc


class Base(abc.ABC):

    @property
    @abc.abstractmethod
    def value(self):
        return 'Should never reach here'

    @value.setter
    @abc.abstractmethod
    def value(self, new_value):
        return


class PartialImplementation(Base):

    @property
    def value(self):
        return 'Read-only'


class Implementation(Base):

    _value = 'Default value'

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value


try:
    b = Base()
    print('Base.value:', b.value)
except Exception as err:
    print('ERROR:', str(err))

p = PartialImplementation()
print('PartialImplementation.value:', p.value)

try:
    p.value = 'Alteration'
    print('PartialImplementation.value:', p.value)
except Exception as err:
    print('ERROR:', str(err))

i = Implementation()
print('Implementation.value:', i.value)

i.value = 'New value'
print('Changed value:', i.value)

La propiedad concreta debe definirse de la misma manera que la propiedad abstracta, ya sea de lectura-escritura o de solo lectura. Anular una propiedad de lectura y escritura en PartialImplementation con una que sea de solo lectura deja la propiedad de solo lectura: el método de establecimiento de la propiedad de la clase base no se reutiliza.

$ python3 abc_abstractproperty_rw.py

ERROR: Can't instantiate abstract class Base with abstract
methods value
PartialImplementation.value: Read-only
ERROR: can't set attribute
Implementation.value: Default value
Changed value: New value

Para usar la sintaxis del decorador con propiedades abstractas de lectura-escritura, los métodos para obtener y establecer el valor deben tener el mismo nombre.

Clases abstractas y métodos estáticos

Los métodos de clase y estáticos también se pueden marcar como abstractos.

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 'Should never reach here'


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 'Static behavior differs'


try:
    o = Base.factory()
    print('Base.value:', o.const_behavior())
except Exception as err:
    print('ERROR:', str(err))

i = Implementation.factory()
print('Implementation.const_behavior :', i.const_behavior())

Aunque el método de la clase se invoca en la clase en lugar de en una instancia, todavía evita que se instancie la clase si no se define.

$ python3 abc_class_static.py

ERROR: Can't instantiate abstract class Base with abstract
methods const_behavior, factory
Implementation.const_behavior : Static behavior differs

Ver también