¿Por qué usar clases base abstractas en Python?

213

Debido a que estoy acostumbrado a las viejas formas de escribir pato en Python, no entiendo la necesidad de ABC (clases base abstractas). La ayuda es buena sobre cómo usarlos.

Traté de leer la justificación en el PEP , pero se me pasó por la cabeza. Si estaba buscando un contenedor de secuencia mutable, lo buscaría __setitem__o probablemente trataría de usarlo ( EAFP ). No he encontrado un uso real para el módulo de números , que usa ABC, pero eso es lo más cercano que tengo que entender.

¿Alguien puede explicarme la razón, por favor?

Muhammad Alkarouri
fuente

Respuestas:

162

Version corta

Los ABC ofrecen un mayor nivel de contrato semántico entre los clientes y las clases implementadas.

Versión larga

Existe un contrato entre una clase y sus interlocutores. La clase promete hacer ciertas cosas y tener ciertas propiedades.

Hay diferentes niveles para el contrato.

En un nivel muy bajo, el contrato puede incluir el nombre de un método o su número de parámetros.

En un lenguaje de tipo estático, el compilador haría cumplir ese contrato. En Python, puede usar EAFP o escribir introspección para confirmar que el objeto desconocido cumple con este contrato esperado.

Pero también hay promesas semánticas de alto nivel en el contrato.

Por ejemplo, si hay un __str__()método, se espera que devuelva una representación de cadena del objeto. Se puede borrar todo el contenido del objeto, confirmar la transacción y escupió una página en blanco de la impresora ... pero hay un entendimiento común de lo que debería hacer, descrito en el manual de Python.

Ese es un caso especial, donde el contrato semántico se describe en el manual. ¿Qué debe hacer el print()método? ¿Debería escribir el objeto en una impresora o una línea en la pantalla, o algo más? Depende: debe leer los comentarios para comprender el contrato completo aquí. Una parte del código del cliente que simplemente verifica que el print()método existe ha confirmado parte del contrato: que se puede hacer una llamada al método, pero no que haya un acuerdo sobre la semántica de nivel superior de la llamada.

La definición de una clase base abstracta (ABC) es una forma de producir un contrato entre los implementadores de la clase y las personas que llaman. No es solo una lista de nombres de métodos, sino una comprensión compartida de lo que deberían hacer esos métodos. Si hereda de este ABC, promete seguir todas las reglas descritas en los comentarios, incluida la semántica del print()método.

La escritura de pato de Python tiene muchas ventajas en flexibilidad sobre la escritura estática, pero no resuelve todos los problemas. Los ABC ofrecen una solución intermedia entre la forma libre de Python y la esclavitud y disciplina de un lenguaje de tipo estático.

Pensamiento extraño
fuente
12
Creo que tienes un punto allí, pero no puedo seguirte. Entonces, ¿cuál es la diferencia, en términos de contrato, entre una clase que implementa __contains__y una clase que hereda collections.Container? En su ejemplo, en Python siempre hubo una comprensión compartida de __str__. Implementar __str__hace las mismas promesas que heredar de algunos ABC y luego implementar __str__. En ambos casos puedes romper el contrato; No hay semánticas demostrables como las que tenemos en la escritura estática.
Muhammad Alkarouri
15
collections.Containeres un caso degenerado, que solo incluye \_\_contains\_\_, y solo significa la convención predefinida. Usar un ABC no agrega mucho valor por sí mismo, estoy de acuerdo. Sospecho que se agregó para permitir (por ejemplo) Setheredar de él. Para cuando llegues Set, pertenecer repentinamente al ABC tiene una semántica considerable. Un artículo no puede pertenecer a la colección dos veces. Eso NO es detectable por la existencia de métodos.
Pensamiento extraño
3
Sí, creo que Setes un mejor ejemplo que print(). Intentaba encontrar un nombre de método cuyo significado fuera ambiguo, y no podía ser asimilado solo por el nombre, por lo que no podía estar seguro de que haría lo correcto solo por su nombre y el manual de Python.
Pensamiento extraño
3
¿Alguna posibilidad de reescribir la respuesta Setcomo ejemplo en lugar de print? Settiene mucho sentido, @Oddthinking.
Ehtesh Choudhury
1
Creo que este artículo lo explica muy bien: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

La respuesta de @ Oddthinking no es incorrecta, pero creo que pierde la razón real y práctica por la que Python tiene ABC en un mundo de escritura de patos.

Los métodos abstractos son geniales, pero en mi opinión, realmente no llenan ningún caso de uso que no esté cubierto por la escritura de patos. El verdadero poder de las clases base abstractas radica en la forma en que le permiten personalizar el comportamiento de isinstanceyissubclass . ( __subclasshook__es básicamente una API más amigable además de Python __instancecheck__y sus__subclasscheck__ ganchos). Adaptar construcciones integradas para trabajar en tipos personalizados es una parte muy importante de la filosofía de Python.

El código fuente de Python es ejemplar. Aquí es cómo collections.Containerse define en la biblioteca estándar (en el momento de la escritura):

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

Esta definición de __subclasshook__dice que cualquier clase con un __contains__atributo se considera una subclase de Contenedor, incluso si no lo hace directamente. Entonces puedo escribir esto:

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

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

En otras palabras, si implementa la interfaz correcta, ¡es una subclase! Los ABC proporcionan una forma formal de definir interfaces en Python, mientras se mantienen fieles al espíritu de tipear patos. Además, esto funciona de una manera que honra el Principio Abierto-Cerrado .

El modelo de objetos de Python se ve superficialmente similar al de un sistema OO más "tradicional" (con el que me refiero a Java *) - obtuvimos sus clases, sus objetos, sus métodos - pero cuando rasca la superficie encontrará algo mucho más rico y mas flexible. Del mismo modo, la noción de Python de clases base abstractas puede ser reconocida por un desarrollador de Java, pero en la práctica están destinadas a un propósito muy diferente.

A veces me encuentro escribiendo funciones polimórficas que pueden actuar sobre un solo elemento o una colección de elementos, y me parece isinstance(x, collections.Iterable)mucho más legible que hasattr(x, '__iter__')un try...exceptbloque equivalente . (Si no conocieras Python, ¿cuál de esos tres aclararía la intención del código?)

Dicho esto, encuentro que rara vez necesito escribir mi propio ABC y normalmente descubro la necesidad de uno a través de la refactorización. Si veo una función polimórfica que realiza muchas comprobaciones de atributos, o muchas funciones que realizan las mismas comprobaciones de atributos, ese olor sugiere la existencia de un ABC a la espera de ser extraído.

* sin entrar en el debate sobre si Java es un sistema OO "tradicional" ...


Anexo : Aunque una clase base abstracta puede anular el comportamiento de isinstancey issubclass, aún no ingresa el MRO de la subclase virtual. Este es un escollo potencial para los clientes: no todos los objetos para los que se isinstance(x, MyABC) == Truehan definido los métodos 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

Desafortunadamente, esta de esas trampas de "simplemente no hagas eso" (¡de las cuales Python tiene relativamente pocas!): Evita definir ABCs con __subclasshook__métodos a y no abstractos. Además, debe hacer que su definición sea __subclasshook__coherente con el conjunto de métodos abstractos que define su ABC.

Benjamin Hodgson
fuente
21
"Si implementa la interfaz correcta, es una subclase" Muchas gracias por eso. No sé si Oddthinking se lo perdió, pero ciertamente lo hice. FWIW, isinstance(x, collections.Iterable)es más claro para mí, y conozco Python.
Muhammad Alkarouri
Excelente post. Gracias. Creo que el Apéndice, "simplemente no hagas eso" trampa, es un poco como hacer una herencia de subclase normal pero luego hacer que la Csubclase elimine (o arruine más allá de la reparación) lo abc_method()heredado MyABC. La principal diferencia es que es la superclase la que está arruinando el contrato de herencia, no la subclase.
Michael Scott Cuthbert
¿No tendría que hacer Container.register(ContainAllTheThings)para que el ejemplo dado funcione?
BoZenKhaa
1
@BoZenKhaa ¡El código en la respuesta funciona! ¡Intentalo! El significado de __subclasshook__es "cualquier clase que satisfaga este predicado se considera una subclase a los efectos isinstancey las issubclasscomprobaciones, independientemente de si se registró en el ABC o de si es una subclase directa ". Como dije en la respuesta, si implementas la interfaz correcta, ¡eres una subclase!
Benjamin Hodgson
1
Debe cambiar el formato de cursiva a negrita. "Si implementa la interfaz correcta, ¡es una subclase!" Explicación muy concisa. ¡Gracias!
Marti
108

Una característica útil de ABC es que si no implementa todos los métodos (y propiedades) necesarios, obtiene un error al crear una instancia, en lugar de un AttributeError, potencialmente mucho más tarde, cuando realmente intenta utilizar el método faltante.

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"

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

Editar: para incluir la sintaxis de python3, gracias @PandasRocks

cerberos
fuente
55
El ejemplo y el enlace fueron muy útiles. ¡Gracias!
nalyd88
si Base se define en un archivo separado, debe heredar de él como Base.Base o cambiar la línea de importación a 'from Base import Base'
Ryan Tennill
Cabe señalar que en Python3 la sintaxis es ligeramente diferente. Vea esta respuesta: stackoverflow.com/questions/28688784/…
PandasRocks
77
Viniendo de un fondo de C #, esta es la razón para usar clases abstractas. Está proporcionando funcionalidad, pero afirmando que la funcionalidad requiere una implementación adicional. Las otras respuestas parecen perder este punto.
Josh Noe
18

Hará mucho más fácil determinar si un objeto admite un protocolo dado sin tener que verificar la presencia de todos los métodos en el protocolo o sin desencadenar una excepción en el territorio "enemigo" debido a la falta de soporte.

Ignacio Vazquez-Abrams
fuente
6

El método abstracto se asegura de que cualquier método que esté llamando en la clase principal tenga que aparecer en la clase secundaria. A continuación se presentan formas noraml de llamar y usar el resumen. El programa escrito en python3

Forma normal de llamar

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 () se llama'

c.methodtwo()

NotImplementedError

Con el método abstracto

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: No se puede crear una instancia de la clase abstracta Son con métodos abstractos methodtwo.

Como no se llama a methodtwo en la clase secundaria, obtuvimos un error. La implementación adecuada está debajo

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 () se llama'

sim
fuente
1
Gracias. ¿No es ese el mismo punto señalado en la respuesta anterior de cerberos?
Muhammad Alkarouri