Orientação a objetos de outra forma: ABC

Série Orientação a objetos de outra forma

Na discussão sobre herança e mixins foram criadas várias classes, como Autenticavel e AutenticavelComRegistro que adicionam funcionalidades a outras classes e implementavam tudo o que precisavam para seu funcionamento. Entretanto podem existir casos em que não seja possível implementar todas as funções na própria classe, deixando com que as classes que a estende implemente essas funções. Uma forma de fazer isso é través das ABC (abstract base classes, ou classes base abstratas).

Sem uso de classes base abstratas

Um exemplo de classe que não é possível implementar todas as funcionalidades foi dada no texto Encapsulamento da lógica do algoritmo, que discutia a leitura de valores do teclado até que um valor válido fosse lido (ou que repete a leitura caso um valor inválido tivesse sido informado). Nesse caso a classe ValidaInput implementava a lógica base de funcionamento, porém eram suas classes filhas (ValidaNomeInput e ValidaNotaInput) que implementavam as funções para tratar o que foi lido do teclado e verificar se é um valor válido ou não.

class ValidaInput:
    mensagem_valor_invalido = 'Valor inválido!'

    def ler_entrada(self, prompt):
        return input(prompt)

    def transformar_entrada(self, entrada):
        raise NotImplementedError

    def validar_valor(self, valor):
        raise NotImplementedError

    def __call__(self, prompt):
        while True:
            try:
                valor = self.transformar_entrada(self.ler_entrada(prompt))
                if self.validar_valor(valor):
                    break
            except ValueError:
                ...
            print(self.mensagem_valor_invalido)
        return valor


class ValidaNomeInput(ValidaInput):
    mensagem_valor_invalido = 'Nome inválido!'

    def transformar_entrada(self, entrada):
        return entrada.strip().title()

    def validar_valor(self, valor):
        return valor != ''


class ValidaNotaInput(ValidaInput):
    mensagem_valor_invalido = 'Nota inválida!'

    def transformar_entrada(self, entrada):
        return float(entrada)

    def validar_valor(self, valor):
        return 0 <= valor <= 10

Entretanto, esse código permite a criação de objetos da classe ValidaInput mesmo sem ter uma implementação das funções transformar_entrada e validar_valor. E a única mensagem de erro ocorreria ao tentar executar essas funções, o que poderia estar longe do problema real, que é a criação de um objeto a partir de uma classe que não prove todas as implementações das suas funções, o que seria semelhante a uma classe abstrata em outras linguagens.

obj = ValidaInput()

# Diversas linhas de código

obj('Entrada: ')  # Exceção NotImplementedError lançada

Com uso de classes base abstratas

Seguindo a documentação da ABC, para utilizá-las é necessário informar a metaclasse ABCMeta na criação da classe, ou simplesmente estender a classe ABC, e decorar com abstractmethod as funções que as classes que a estenderem deverão implementar. Exemplo:

from abc import ABC, abstractmethod


class ValidaInput(ABC):
    mensagem_valor_invalido = 'Valor inválido!'

    def ler_entrada(self, prompt):
        return input(prompt)

    @abstractmethod
    def transformar_entrada(self, entrada):
        ...

    @abstractmethod
    def validar_valor(self, valor):
        ...

    def __call__(self, prompt):
        while True:
            try:
                valor = self.transformar_entrada(self.ler_entrada(prompt))
                if self.validar_valor(valor):
                    break
            except ValueError:
                ...
            print(self.mensagem_valor_invalido)
        return valor

Desta forma, ocorrerá um erro já ao tentar criar um objeto do tipo ValidaInput, dizendo quais são as funções que precisam ser implementadas. Porém funcionará normalmente ao criar objetos a partir das classes ValidaNomeInput e ValidaNotaInput visto que elas implementam essas funções.

obj = ValidaInput()  # Exceção TypeError lançada

nome_input = ValidaNomeInput()  # Objeto criado
nota_input = ValidaNotaInput()  # Objeto criado

Como essas funções não utilizam a referência ao objeto (self), ainda é possível decorar as funções com staticmethod, como:

from abc import ABC, abstractmethod


class ValidaInput(ABC):
    mensagem_valor_invalido = 'Valor inválido!'

    @staticmethod
    def ler_entrada(prompt):
        return input(prompt)

    @staticmethod
    @abstractmethod
    def transformar_entrada(entrada):
        ...

    @staticmethod
    @abstractmethod
    def validar_valor(valor):
        ...

    def __call__(self, prompt):
        while True:
            try:
                valor = self.transformar_entrada(self.ler_entrada(prompt))
                if self.validar_valor(valor):
                    break
            except ValueError:
                ...
            print(self.mensagem_valor_invalido)
        return valor


class ValidaNomeInput(ValidaInput):
    mensagem_valor_invalido = 'Nome inválido!'

    @staticmethod
    def transformar_entrada(entrada):
        return entrada.strip().title()

    @staticmethod
    def validar_valor(valor):
        return valor != ''


class ValidaNotaInput(ValidaInput):
    mensagem_valor_invalido = 'Nota inválida!'

    @staticmethod
    def transformar_entrada(entrada):
        return float(entrada)

    @staticmethod
    def validar_valor(valor):
        return 0 <= valor <= 10

Isso também seria válido para funções decoradas com classmethod, que receberiam a referência a classe (cls).

Considerações

Não é necessário utilizar ABC para fazer o exemplo discutido, porém ao utilizar essa biblioteca ficou mais explícito quais as funções que precisavam ser implementados nas classes filhas, ainda mais que sem utilizar ABC a classe base poderia nem ter as funções, com:

class ValidaInput:
    mensagem_valor_invalido = 'Valor inválido!'

    def ler_entrada(self, prompt):
        return input(prompt)

    def __call__(self, prompt):
        while True:
            try:
                valor = self.transformar_entrada(self.ler_entrada(prompt))
                if self.validar_valor(valor):
                    break
            except ValueError:
                ...
            print(self.mensagem_valor_invalido)
        return valor

Como Python possui duck-typing, não é necessário uma grande preocupação com os tipos, como definir e utilizar interfaces presentes em outras implementações de orientação a objetos, porém devido à herança múltipla, ABC pode ser utilizada como interface que não existe em Python, fazendo com que as classes implementem determinadas funções. Para mais a respeito desse assunto, recomendo as duas lives do dunossauro sobre ABC (1 e 2), e a apresentação do Luciano Ramalho sobre type hints.

Uma classe filha também não é obrigada a implementar todas as funções decoradas com abstractmethod, mas assim como a classe pai, não será possível criar objetos a partir dessa classe, apenas de uma classe filha dela que implemente as demais funções. Como se ao aplicar um abstractmethod tornasse a classe abstrata, e qualquer classe filha só deixasse de ser abstrata quando a última função decorada com abstractmethod for sobrescrita. Exemplo:

from abc import ABC, abstractmethod


class A(ABC):
    @abstractmethod
    def func1(self):
        ...

    @abstractmethod
    def func2(self):
        ...


class B(A):
    def func1(self):
        print('1')


class C(B):
    def func2(self):
        print('2')


a = A()  # Erro por não implementar func1 e func2
b = B()  # Erro por não implementar func2
c = C()  # Objeto criado