Pourquoi utiliser les classes de base abstraites en Python?

213

Parce que je suis habitué aux anciennes façons de taper du canard en Python, je n'arrive pas à comprendre le besoin d'ABC (classes de base abstraites). L' aide est bonne sur la façon de les utiliser.

J'ai essayé de lire la justification du PEP , mais cela m'a dépassé la tête. Si je cherchais un conteneur de séquence mutable, je vérifierais __setitem__ou, plus probablement, j'essaierais de l'utiliser ( EAFP ). Je ne suis pas tombé sur une utilisation réelle du module de nombres , qui utilise des ABC, mais c'est le plus proche que j'ai à comprendre.

Quelqu'un peut-il m'expliquer la justification, s'il vous plaît?

Muhammad Alkarouri
la source

Réponses:

162

Version courte

Les ABC offrent un niveau de contrat sémantique plus élevé entre les clients et les classes implémentées.

Version longue

Il existe un contrat entre une classe et ses appelants. La classe promet de faire certaines choses et d'avoir certaines propriétés.

Le contrat comporte différents niveaux.

À un niveau très bas, le contrat peut inclure le nom d'une méthode ou son nombre de paramètres.

Dans un langage de type statique, ce contrat serait effectivement appliqué par le compilateur. En Python, vous pouvez utiliser EAFP ou taper introspection pour confirmer que l'objet inconnu respecte ce contrat attendu.

Mais il y a aussi des promesses sémantiques de plus haut niveau dans le contrat.

Par exemple, s'il existe une __str__()méthode, elle devrait renvoyer une représentation sous forme de chaîne de l'objet. Il pourrait supprimer tout le contenu de l'objet, valider la transaction et cracher une page vierge hors de l'imprimante ... mais il y a une compréhension commune de ce qu'il devrait faire, décrite dans le manuel Python.

C'est un cas particulier, où le contrat sémantique est décrit dans le manuel. Que doit faire la print()méthode? Doit-il écrire l'objet sur une imprimante ou une ligne sur l'écran, ou autre chose? Cela dépend - vous devez lire les commentaires pour comprendre l'intégralité du contrat ici. Un morceau de code client qui vérifie simplement que la print()méthode existe a confirmé une partie du contrat - qu'un appel de méthode peut être effectué, mais pas qu'il existe un accord sur la sémantique de niveau supérieur de l'appel.

La définition d'une classe de base abstraite (ABC) est un moyen de produire un contrat entre les implémenteurs de classe et les appelants. Ce n'est pas seulement une liste de noms de méthodes, mais une compréhension partagée de ce que ces méthodes doivent faire. Si vous héritez de cet ABC, vous vous engagez à suivre toutes les règles décrites dans les commentaires, y compris la sémantique de la print()méthode.

Le typage canard de Python présente de nombreux avantages en termes de flexibilité par rapport au typage statique, mais il ne résout pas tous les problèmes. Les ABC offrent une solution intermédiaire entre la forme libre de Python et l'esclavage et la discipline d'un langage de type statique.

Pensée étrange
la source
12
Je pense que vous avez raison ici, mais je ne peux pas vous suivre. Quelle est donc la différence, en termes de contrat, entre une classe qui implémente __contains__et une classe qui hérite collections.Container? Dans votre exemple, en Python, il y avait toujours une compréhension partagée de __str__. L'implémentation __str__fait les mêmes promesses que l'héritage de certains ABC puis l'implémentation __str__. Dans les deux cas, vous pouvez rompre le contrat; il n'y a pas de sémantique prouvable comme celles que nous avons en typage statique.
Muhammad Alkarouri
15
collections.Containerest un cas dégénéré, qui n'inclut \_\_contains\_\_, et ne signifie que la convention prédéfinie. L'utilisation d'un ABC n'ajoute pas beaucoup de valeur en soi, je suis d'accord. Je soupçonne qu'il a été ajouté pour permettre (par exemple) Setd'en hériter. Au moment où vous y arrivez Set, l'appartenance soudaine à l'ABC a une sémantique considérable. Un article ne peut pas appartenir deux fois à la collection. Ce n'est PAS détectable par l'existence de méthodes.
Oddthinking
3
Oui, je pense que Setc'est un meilleur exemple que print(). J'essayais de trouver un nom de méthode dont la signification était ambiguë, et ne pouvait pas être bloqué par le seul nom, donc vous ne pouviez pas être sûr qu'il ferait la bonne chose juste par son nom et le manuel Python.
Pensée étrange
3
Une chance de réécrire la réponse avec Setcomme exemple au lieu de print? Seta beaucoup de sens, @Oddthinking.
Ehtesh Choudhury
1
Je pense que cet article l'explique très bien: dbader.org/blog/abstract-base-classes-in-python
szabgab
228

@ La réponse de Oddthinking n'est pas mal, mais je pense qu'il manque le réel , pratique la raison Python a ABCs dans un monde de canard frappe.

Les méthodes abstraites sont soignées, mais à mon avis, elles ne remplissent pas vraiment les cas d'utilisation qui ne sont pas déjà couverts par le typage canard. La puissance réelle des classes de base abstraites réside dans la façon dont elles vous permettent de personnaliser le comportement de isinstanceetissubclass . ( __subclasshook__est fondamentalement une API plus conviviale en plus de Python __instancecheck__et des__subclasscheck__ hooks.) L'adaptation de constructions intégrées pour travailler sur des types personnalisés fait partie intégrante de la philosophie de Python.

Le code source de Python est exemplaire. Voici comment collections.Containerest défini dans la bibliothèque standard (au moment de la rédaction):

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

Cette définition de __subclasshook__ dit que toute classe avec un __contains__attribut est considérée comme une sous-classe de Container, même si elle ne le sous-classe pas directement. Je peux donc écrire ceci:

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

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

En d'autres termes, si vous implémentez la bonne interface, vous êtes une sous-classe! Les ABC fournissent un moyen formel de définir des interfaces en Python, tout en restant fidèle à l'esprit de la frappe de canard. En outre, cela fonctionne d'une manière qui respecte le principe ouvert-fermé .

Le modèle objet de Python ressemble superficiellement à celui d'un système OO plus "traditionnel" (par lequel je veux dire Java *) - nous avons des classes yer, des objets yer, des méthodes yer - mais lorsque vous grattez la surface, vous trouverez quelque chose de beaucoup plus riche et plus flexible. De même, la notion de Python de classes de base abstraites peut être reconnaissable par un développeur Java, mais en pratique, elles sont destinées à un objectif très différent.

Je me retrouve parfois à écrire des fonctions polymorphes qui peuvent agir sur un seul élément ou une collection d'éléments, et je trouve isinstance(x, collections.Iterable) être beaucoup plus lisible que hasattr(x, '__iter__')ou un try...exceptbloc équivalent . (Si vous ne connaissiez pas Python, lequel de ces trois rendrait l'intention du code plus claire?)

Cela dit, je trouve que j'ai rarement besoin d'écrire mon propre ABC et je découvre généralement le besoin d'un refactoring. Si je vois une fonction polymorphe faire beaucoup de vérifications d'attributs, ou beaucoup de fonctions faire les mêmes vérifications d'attributs, cette odeur suggère l'existence d'un ABC en attente d'extraction.

* sans entrer dans le débat sur la question de savoir si Java est un système OO "traditionnel" ...


Addendum : Même si une classe de base abstraite peut remplacer le comportement de isinstanceand issubclass, elle n'entre toujours pas dans le MRO de la sous-classe virtuelle. C'est un piège potentiel pour les clients: tous les objets pour lesquels isinstance(x, MyABC) == Trueles méthodes sont définies ne sont pas tous définis 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

Malheureusement, celui-ci de ces pièges «ne faites pas ça» (dont Python a relativement peu!): Évitez de définir ABC avec __subclasshook__des méthodes a et non abstraites. De plus, vous devez rendre votre définition de __subclasshook__cohérente avec l'ensemble des méthodes abstraites définies par votre ABC.

Benjamin Hodgson
la source
21
"si vous implémentez la bonne interface, vous êtes une sous-classe" Merci beaucoup pour cela. Je ne sais pas si Oddthinking l'a raté, mais je l'ai certainement fait. FWIW, isinstance(x, collections.Iterable)c'est plus clair pour moi, et je connais Python.
Muhammad Alkarouri
Excellent poste. Je vous remercie. Je pense que l'addendum, "ne faites pas ça", c'est un peu comme faire un héritage normal de sous-classe, mais avoir ensuite la Csous - classe supprimée (ou foiré au-delà de toute réparation) dont elle a abc_method()hérité MyABC. La principale différence est que c'est la superclasse qui fout le contrat de succession, pas la sous-classe.
Michael Scott Cuthbert,
N'auriez-vous pas à faire Container.register(ContainAllTheThings)pour que l'exemple donné fonctionne?
BoZenKhaa
1
@BoZenKhaa Le code dans la réponse fonctionne! Essayez! La signification de __subclasshook__est "toute classe qui satisfait à ce prédicat est considérée comme une sous-classe aux fins de isinstanceet issubclassvérifie, qu'elle soit enregistrée auprès de l'ABC et qu'elle soit une sous-classe directe ". Comme je l'ai dit dans la réponse, si vous implémentez la bonne interface, vous êtes une sous-classe!
Benjamin Hodgson
1
Vous devez changer la mise en forme de l'italique en gras. "si vous implémentez la bonne interface, vous êtes une sous-classe!" Explication très concise. Je vous remercie!
marti
108

Une caractéristique pratique des ABC est que si vous n'implémentez pas toutes les méthodes (et propriétés) nécessaires, vous obtenez une erreur lors de l'instanciation, plutôt qu'un AttributeError, potentiellement beaucoup plus tard, lorsque vous essayez réellement d'utiliser la méthode manquante.

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"

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

Edit: pour inclure la syntaxe python3, merci @PandasRocks

cerberos
la source
5
L'exemple et le lien ont tous deux été très utiles. Merci!
nalyd88
si Base est défini dans un fichier séparé, vous devez en hériter en tant que Base.Base ou changer la ligne d'importation en 'from Base import Base'
Ryan Tennill
Il convient de noter qu'en Python3, la syntaxe est légèrement différente. Voir cette réponse: stackoverflow.com/questions/28688784/…
PandasRocks
7
Venant d'un arrière-plan C #, c'est la raison d'utiliser des classes abstraites. Vous fournissez des fonctionnalités, mais déclarez que la fonctionnalité nécessite une implémentation supplémentaire. Les autres réponses semblent manquer ce point.
Josh Noe
18

Cela rendra plus facile de déterminer si un objet prend en charge un protocole donné sans avoir à vérifier la présence de toutes les méthodes dans le protocole ou sans déclencher une exception profondément en territoire "ennemi" en raison d'un non-support.

Ignacio Vazquez-Abrams
la source
6

La méthode abstraite s'assure que la méthode que vous appelez dans la classe parent doit apparaître dans la classe enfant. Vous trouverez ci-dessous la manière noraml d'appeler et d'utiliser le résumé. Le programme écrit en python3

Manière normale d'appeler

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 () s'appelle'

c.methodtwo()

NotImplementedError

Avec la méthode abstraite

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: Impossible d'instancier la classe abstraite Son avec deux méthodes abstraites.

Puisque methodtwo n'est pas appelé dans la classe enfant, nous avons eu une erreur. La bonne mise en œuvre est ci-dessous

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 () s'appelle'

sim
la source
1
Merci. N'est-ce pas le même point soulevé dans la réponse de Cerberos ci-dessus?
Muhammad Alkarouri