Por que usar classes base abstratas em Python?

213

Como estou acostumado com as formas antigas de digitação de patos no Python, não entendo a necessidade de ABC (classes básicas abstratas). A ajuda é boa sobre como usá-los.

Tentei ler a lógica do PEP , mas isso passou por minha cabeça. Se eu estivesse procurando um contêiner de sequência mutável, procuraria __setitem__ou provavelmente tentaria usá-lo ( EAFP ). Não encontrei um uso da vida real para o módulo de números , que usa ABCs, mas esse é o mais próximo que posso entender.

Alguém pode me explicar o raciocínio, por favor?

Muhammad Alkarouri
fonte

Respostas:

162

Versão curta

Os ABCs oferecem um nível mais alto de contrato semântico entre clientes e as classes implementadas.

Versão longa

Existe um contrato entre uma classe e seus interlocutores. A turma promete fazer certas coisas e ter certas propriedades.

Existem diferentes níveis no contrato.

Em um nível muito baixo, o contrato pode incluir o nome de um método ou o número de parâmetros.

Em uma linguagem de tipo estático, esse contrato seria realmente aplicado pelo compilador. No Python, você pode usar o EAFP ou digitar introspecção para confirmar se o objeto desconhecido atende a esse contrato esperado.

Mas também existem promessas semânticas de nível superior no contrato.

Por exemplo, se houver um __str__()método, espera-se retornar uma representação de seqüência de caracteres do objeto. Ele pode excluir todo o conteúdo do objeto, confirmar a transação e cuspir uma página em branco para fora da impressora ... mas existe um entendimento comum do que ele deve fazer, descrito no manual do Python.

Esse é um caso especial, onde o contrato semântico é descrito no manual. O que o print()método deve fazer? Ele deve gravar o objeto em uma impressora ou uma linha na tela, ou algo mais? Depende - você precisa ler os comentários para entender o contrato completo aqui. Uma parte do código do cliente que simplesmente verifica se o print()método existe confirmou parte do contrato - que uma chamada de método pode ser feita, mas não que haja acordo sobre a semântica de nível superior da chamada.

Definir uma classe base abstrata (ABC) é uma maneira de produzir um contrato entre os implementadores de classe e os chamadores. Não é apenas uma lista de nomes de métodos, mas um entendimento compartilhado do que esses métodos devem fazer. Se você herdar deste ABC, promete seguir todas as regras descritas nos comentários, incluindo a semântica do print()método.

A digitação de pato do Python tem muitas vantagens em flexibilidade sobre a digitação estática, mas não resolve todos os problemas. Os ABCs oferecem uma solução intermediária entre a forma livre do Python e a escravidão e a disciplina de uma linguagem estática.

Oddthinking
fonte
12
Eu acho que você tem razão, mas não posso segui-lo. Então, qual é a diferença, em termos de contrato, entre uma classe que implementa __contains__e uma classe que herda collections.Container? No seu exemplo, no Python sempre havia um entendimento compartilhado __str__. Implementar __str__faz as mesmas promessas que herdar de algum ABC e depois implementá-lo __str__. Nos dois casos, você pode quebrar o contrato; não há semânticas prováveis, como as que temos na digitação estática.
Muhammad Alkarouri 26/08/10
15
collections.Containeré um caso degenerado, que inclui \_\_contains\_\_apenas e significa apenas a convenção predefinida. Usar um ABC não agrega muito valor por si só, eu concordo. Eu suspeito que foi adicionado para permitir (por exemplo) Setherdar dele. Quando você chega Set, de repente pertencer ao ABC tem uma semântica considerável. Um item não pode pertencer à coleção duas vezes. Isso NÃO é detectável pela existência de métodos.
Oddthinking 26/08/10
3
Sim, acho que Seté um exemplo melhor do que print(). Eu estava tentando encontrar um nome de método cujo significado fosse ambíguo e não pudesse ser grupado apenas pelo nome, então você não podia ter certeza de que ele faria a coisa certa apenas pelo nome e pelo manual do Python.
Oddthinking 28/08/10
3
Alguma chance de reescrever a resposta com Seto exemplo em vez de print? Setfaz muito sentido, @Oddthinking.
Ehtesh Choudhury
1
Acho que este artigo explica isso muito bem: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

@ A resposta de Oddthinking não está errada, mas acho que ela perde a verdadeira e prática razão pela qual o Python possui ABCs em um mundo de digitação de patos.

Os métodos abstratos são legais, mas, na minha opinião, eles realmente não preenchem nenhum caso de uso ainda não coberto pela digitação do duck. O poder real das classes base abstratas reside na maneira como elas permitem que você personalize o comportamento de isinstanceeissubclass . ( __subclasshook__é basicamente uma API mais amigável em cima do Python __instancecheck__e do__subclasscheck__ hooks.) A adaptação de construções internas para trabalhar em tipos personalizados faz parte da filosofia do Python.

O código fonte do Python é exemplar. Aqui está como collections.Containeré definido na biblioteca padrão (no momento da redação):

class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Essa definição __subclasshook__diz que qualquer classe com um __contains__atributo é considerada uma subclasse de Container, mesmo que não a subclasse diretamente. Então eu posso escrever isso:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

Em outras palavras, se você implementar a interface certa, você é uma subclasse! Os ABCs fornecem uma maneira formal de definir interfaces em Python, mantendo-se fiel ao espírito da digitação de patos. Além disso, isso funciona de uma maneira que honra o Princípio Aberto-Fechado .

O modelo de objeto do Python parece superficialmente semelhante ao de um sistema OO mais "tradicional" (com o que quero dizer Java *) - temos classes, objetos e métodos - mas quando você arranha a superfície, encontra algo muito mais rico e mais flexível. Da mesma forma, a noção de classes abstratas de base do Python pode ser reconhecida por um desenvolvedor Java, mas na prática elas se destinam a uma finalidade muito diferente.

Às vezes, me pego escrevendo funções polimórficas que podem atuar em um único item ou em uma coleção de itens, e acho isinstance(x, collections.Iterable)que é muito mais legível do que hasattr(x, '__iter__')ou um try...exceptbloco equivalente . (Se você não conhecia Python, qual desses três tornaria a intenção do código mais clara?)

Dito isto, acho que raramente preciso escrever meu próprio ABC e normalmente descubro a necessidade de um através da refatoração. Se eu vir uma função polimórfica fazendo muitas verificações de atributos ou muitas funções fazendo as mesmas verificações de atributos, esse cheiro sugere a existência de um ABC aguardando a extração.

* sem entrar em debate sobre se o Java é um sistema OO "tradicional" ...


Adenda : Mesmo que uma classe base abstrata pode substituir o comportamento de isinstancee issubclass, ainda não entra no MRO da subclasse virtual. Essa é uma armadilha em potencial para os clientes: nem todo objeto para o qual isinstance(x, MyABC) == Trueos métodos estão definidos MyABC.

class MyABC(metaclass=abc.ABCMeta):
    def abc_method(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        return True

class C(object):
    pass

# typical client code
c = C()
if isinstance(c, MyABC):  # will be true
    c.abc_method()  # raises AttributeError

Infelizmente, uma dessas armadilhas "simplesmente não faça isso" (das quais o Python tem relativamente poucos!): Evite definir ABCs com __subclasshook__métodos a e não abstratos. Além disso, você deve tornar sua definição __subclasshook__consistente com o conjunto de métodos abstratos definidos pelo ABC.

Benjamin Hodgson
fonte
21
"se você implementar a interface certa, você é uma subclasse" Muito obrigado por isso. Não sei se Oddthinking perdeu, mas certamente o fiz. FWIW, isinstance(x, collections.Iterable)é mais claro para mim, e eu conheço Python.
Muhammad Alkarouri 31/10
Excelente post. Obrigado. Eu acho que o Adendo, "simplesmente não faça isso", é um pouco como fazer uma herança de subclasse normal, mas depois fazer com que a Csubclasse exclua (ou estrague sem reparo) os abc_method()herdados de MyABC. A principal diferença é que é a superclasse que está estragando o contrato de herança, não a subclasse.
Michael Scott Cuthbert
Você não precisaria fazer Container.register(ContainAllTheThings)o exemplo dado para funcionar?
BoZenKhaa
1
@BoZenKhaa O código na resposta funciona! Tente! O significado de __subclasshook__"qualquer classe que satisfaça esse predicado é considerada uma subclasse para os fins isinstancee issubclassverificações, independentemente de ter sido registrado no ABC e independentemente de ser uma subclasse direta ". Como eu disse na resposta, se você implementar a interface certa, você é uma subclasse!
Benjamin Hodgson
1
Você deve alterar a formatação de itálico para negrito. "se você implementar a interface certa, você é uma subclasse!" Explicação muito concisa. Obrigado!
marti 31/01
108

Um recurso útil dos ABCs é que, se você não implementar todos os métodos (e propriedades) necessários, ocorrerá um erro na instanciação, em vez de AttributeError, potencialmente muito mais tarde, quando você realmente tentar usar o método ausente.

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
class Base(object, metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`


c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

Exemplo de https://dbader.org/blog/abstract-base-classes-in-python

Edit: para incluir a sintaxe python3, obrigado @PandasRocks

cerberos
fonte
5
O exemplo e o link foram muito úteis. Obrigado!
Nalyd88 29/05
Se a base é definido em um arquivo separado, você deve herdar a partir dele como Base.Base ou alterar a linha de importação para 'da Base de importação Básico'
Ryan Tennill
Note-se que no Python3 a sintaxe é um pouco diferente. Veja esta resposta: stackoverflow.com/questions/28688784/...
PandasRocks
7
Vindo de um background em C #, esse é o motivo para usar classes abstratas. Você está fornecendo funcionalidade, mas informando que a funcionalidade requer implementação adicional. As outras respostas parecem não entender esse ponto.
Josh Noe
18

Isso determinará se um objeto suporta um determinado protocolo sem ter que verificar a presença de todos os métodos no protocolo ou sem desencadear uma exceção no território "inimigo" devido ao não suporte muito mais fácil.

Ignacio Vazquez-Abrams
fonte
6

Método abstrato verifique se qualquer método que você está chamando na classe pai deve aparecer na classe filho. Abaixo estão uma maneira normal de chamar e usar o resumo. O programa escrito em python3

Maneira normal de ligar

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

'methodone () é chamado'

c.methodtwo()

NotImplementedError

Com método abstrato

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError: Não é possível instanciar a classe abstrata Son com métodos abstratos methodtwo.

Como methodtwo não é chamado na classe filho, obtemos erro. A implementação adequada está abaixo

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

'methodone () é chamado'

sim
fonte
1
Obrigado. Não é esse o mesmo argumento exposto em cerberos acima?
Muhammad Alkarouri