web-dev-qa-db-fra.com

Pourquoi utiliser les classes de base abstraites en Python?

Étant donné que je suis habitué aux anciennes méthodes de frappe au canard en Python, je ne comprends pas la nécessité d’ABC (classes de base abstraites). Le aide explique comment les utiliser.

J'ai essayé de lire le raisonnement dans le PEP , mais cela m'est passé par dessus la tête. Si je cherchais un conteneur de séquence mutable, je vérifierais __setitem__, ou plus probablement, essayez de l’utiliser ( EAFP ). Je n'ai pas rencontré d'utilisation concrète pour le module numbers , qui utilise ABC, mais c'est ce que j'ai de plus proche de la compréhension.

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

187
Muhammad Alkarouri

Version courte

ABC offre un niveau plus élevé de contrat sémantique 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 en réalité appliqué par le compilateur. En Python, vous pouvez utiliser EAFP ou l'introspection pour confirmer que l'objet inconnu respecte le contrat attendu.

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

Par exemple, s'il existe une méthode __str__(), 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 de l'imprimante ... mais il existe une compréhension commune de ce qu'elle doit faire, décrit dans la Python manuel.

C'est un cas spécial, où le contrat sémantique est décrit dans le manuel. Que doit faire la méthode print()? Devrait-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 le contrat en entier ici. Une partie du code client qui vérifie simplement que la méthode print() existe a confirmé une partie du contrat - qu'un appel de méthode peut être effectué, mais non 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) permet de créer un contrat entre les développeurs et les appelants. Ce n'est pas simplement une liste de noms de méthodes, mais une compréhension partagée de ce que ces méthodes devraient 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 méthode print().

Le typage de 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. ABC offre une solution intermédiaire entre la forme libre de Python et la servitude et la discipline d’un langage à typage statique).

141
Oddthinking

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

Les méthodes abstraites sont bien nettes, mais à mon avis, elles ne remplissent pas vraiment les cas d'utilisation qui ne sont pas déjà couverts par la saisie au clavier. Le pouvoir réel des classes de base abstraites réside dans la façon dont elles permettent de personnaliser le comportement de isinstance et issubclass . (__subclasshook__ Est fondamentalement une API plus conviviale que les crochets __instancecheck__ Et __subclasscheck__ de Python. L'adaptation de constructions intégrées pour travailler sur des types personnalisés est fait partie de la philosophie de Python.

Le code source de Python est exemplaire. Ici voici comment collections.Container Est défini dans la bibliothèque standard (au moment de l'écriture):

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__ Indique que toute classe ayant un attribut __contains__ Est considérée comme une sous-classe de Container, même si elle ne la sous-classe pas directement. Donc je peux é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èles à l'esprit du canard. En outre, cela fonctionne d'une manière qui honore le principe ouvert-fermé .

Le modèle d'objet de Python ressemble en apparence à celui d'un système plus "traditionnel" OO (par lequel je veux dire Java *) - nous avons vos classes, vos objets, vos méthodes - mais lorsque vous grattez le De plus, la notion de classes de base abstraites de Python peut être reconnue par un développeur Java), mais elle est en réalité destinée à un but très différent.

Je me trouve parfois en train d'écrire des fonctions polymorphes pouvant agir sur un seul élément ou sur une collection d'éléments et je trouve que isinstance(x, collections.Iterable) est beaucoup plus lisible que hasattr(x, '__iter__') ou un équivalent try...except bloquer. (Si vous ne connaissiez pas Python, lequel de ces trois dirait le plus clairement l'intention du code?)

Cela dit, je constate que j’ai rarement besoin d’écrire mon propre ABC et je découvre généralement la nécessité de le faire par le biais de la refactorisation. Si je vois une fonction polymorphe effectuer de nombreuses vérifications d'attributs, ou de nombreuses fonctions effectuant 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 pour savoir si Java est un système "traditionnel" OO système ...


Addendum : Même si une classe de base abstraite peut remplacer le comportement de isinstance et issubclass, elle ne l'a toujours pas été. entrez le MRO de la sous-classe virtuelle. Il s'agit d'un écueil potentiel pour les clients: il ne s'agit pas de tous les objets pour lesquels isinstance(x, MyABC) == True possède les méthodes définies sur 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, l’un de ces pièges "juste ne fais pas ça" (dont Python en a relativement peu!): Évite de définir ABC avec des méthodes __subclasshook__ Et non abstraites De plus, vous devriez rendre votre définition de __subclasshook__ Cohérente avec l'ensemble des méthodes abstraites définies par votre ABC.

205
Benjamin Hodgson

Une caractéristique pratique d’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 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

95
cerberos

Il sera beaucoup 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 du protocole ou sans déclencher une exception au plus profond du territoire "ennemi" en raison d'un non-support.

16

Méthode abstraite assurez-vous que la méthode que vous appelez dans la classe parente doit apparaître dans la classe enfant. Vous trouverez ci-dessous une manière noraml d’appeler et d’utiliser abstract. 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 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 les méthodes abstraites methodtwo.

Puisque methodtwo n'est pas appelé dans la classe enfant, nous avons 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'

5
sim