web-dev-qa-db-fra.com

Java conception abstraite / interface en Python

J'ai un certain nombre de classes qui partagent toutes les mêmes méthodes, uniquement avec des implémentations différentes. En Java, il serait logique que chacune de ces classes implémente une interface ou étende une classe abstraite. Est-ce que Python a quelque chose de similaire à ça, ou devrais-je adopter une approche alternative?

38
Matt

Il y a une petite histoire derrière les interfaces en Python. L'attitude originale, qui a régné pendant de nombreuses années, est que vous n'en avez pas besoin: Python fonctionne sur le principe EAFP (plus facile de demander pardon que la permission). Autrement dit, au lieu de spécifier que vous acceptez un objet ICloseable, je ne sais pas, vous essayez simplement de close l'objet lorsque vous en avez besoin, et s'il lève une exception, alors il lève une exception.

Donc, dans cette mentalité, vous écririez simplement vos cours séparément et vous les utiliseriez comme vous voudrez. Si l'un d'eux n'est pas conforme aux exigences, votre programme lèvera une exception; inversement, si vous écrivez une autre classe avec les bonnes méthodes, cela fonctionnera simplement, sans que vous ayez besoin de spécifier qu'elle implémente votre interface particulière.

Cela fonctionne plutôt bien, mais il existe des cas d'utilisation précis des interfaces, en particulier avec des projets logiciels plus importants. La décision finale dans Python était de fournir le module abc , qui vous permet d'écrire des classes de base abstraites c'est-à-dire des classes que vous ne pouvez pas instancier à moins de remplacer toutes leurs méthodes. C'est à vous de décider si vous pensez que leur utilisation en vaut la peine.

Le PEP présentant ABCs explique beaucoup mieux que je ne peux:

Dans le domaine de la programmation orientée objet, les modèles d'utilisation pour interagir avec un objet peuvent être divisés en deux catégories de base, qui sont "invocation" et "inspection".

L'appel signifie interagir avec un objet en appelant ses méthodes. Habituellement, cela est combiné avec le polymorphisme, de sorte que l'invocation d'une méthode donnée peut exécuter un code différent selon le type d'un objet.

L'inspection signifie la possibilité pour le code externe (en dehors des méthodes de l'objet) d'examiner le type ou les propriétés de cet objet et de prendre des décisions sur la façon de traiter cet objet en fonction de ces informations.

Les deux modèles d'utilisation servent le même objectif général, qui est de pouvoir prendre en charge le traitement d'objets divers et potentiellement nouveaux de manière uniforme, tout en permettant en même temps de personnaliser les décisions de traitement pour chaque type d'objet différent.

Dans la théorie classique OOP), l'invocation est le modèle d'utilisation préféré, et l'inspection est activement découragée, étant considérée comme une relique d'un style de programmation procédural antérieur. Cependant, en pratique, cette vue est tout simplement trop dogmatique et inflexible, et conduit à une sorte de rigidité de conception qui est très en contradiction avec la nature dynamique d'un langage comme Python.

En particulier, il est souvent nécessaire de traiter les objets d'une manière qui n'a pas été prévue par le créateur de la classe d'objets. Ce n'est pas toujours la meilleure solution pour intégrer toutes les méthodes d'objet qui satisfont les besoins de chaque utilisateur possible de cet objet. De plus, il existe de nombreuses philosophies de répartition puissantes qui sont en contraste direct avec l'exigence classique OOP d'un comportement strictement encapsulé dans un objet, les exemples étant une logique basée sur des règles ou des correspondances de modèles.

D'autre part, l'une des critiques de l'inspection par les théoriciens classiques OOP) est le manque de formalismes et le caractère ad hoc de ce qui est inspecté. Dans un langage comme Python, dans lequel presque n'importe quel aspect d'un objet peut être reflété et directement accessible par un code externe, il existe de nombreuses façons différentes de tester si un objet est conforme ou non à un protocole particulier. Par exemple, si vous demandez "cet objet est-il un conteneur de séquence mutable?", un peut chercher une classe de base de 'list', ou on peut chercher une méthode nommée '__getitem__'. Mais notez que bien que ces tests puissent sembler évidents, aucun d'eux n'est correct, car l'un génère des faux négatifs, et les autres faux positifs .

Le remède généralement accepté est de normaliser les tests et de les regrouper dans un arrangement formel. Cela se fait le plus facilement en associant à chaque classe un ensemble de propriétés testables standard, soit via le mécanisme d'héritage, soit par d'autres moyens. Chaque test comporte un ensemble de promesses: il contient une promesse sur le comportement général de la classe, et une promesse sur les autres méthodes de classe qui seront disponibles.

Ce PEP propose une stratégie particulière pour l'organisation de ces tests appelée classes de base abstraites, ou ABC. Les ABC sont simplement Python classes qui sont ajoutées dans l'arbre d'héritage d'un objet pour signaler certaines caractéristiques de cet objet à un inspecteur externe. Les tests sont effectués en utilisant isinstance (), et la présence d'un ABC particulier signifie que le test a réussi.

De plus, les ABC définissent un ensemble minimal de méthodes qui établissent le comportement caractéristique du type. Le code qui discrimine les objets en fonction de leur type ABC peut être sûr que ces méthodes seront toujours présentes. Chacune de ces méthodes est accompagnée d'une définition sémantique abstraite généralisée qui est décrite dans la documentation de l'ABC. Ces définitions sémantiques standard ne sont pas appliquées, mais sont fortement recommandées.

Comme toutes les autres choses en Python, ces promesses sont de la nature d'un gentlemen's agreement, ce qui dans ce cas signifie que, bien que le langage applique certaines des promesses faites dans l'ABC, c'est à l'implémenteur de la classe concrète de s'assurer que les autres sont conservés.

63
Katriel

Je ne suis pas très familier avec Python, mais je risquerais de penser que ce n'est pas le cas.

La raison pour laquelle les interfaces existent dans Java est qu'elles spécifient un contrat. Quelque chose qui implémente Java.util.List, Par exemple, est garanti d'avoir un add() pour se conformer au comportement général défini sur l'interface. Vous pouvez supprimer toute implémentation (sensée) de List sans connaître sa classe spécifique, appeler une séquence de méthodes définies sur l'interface et obtenir le même comportement général .

De plus, le développeur et le compilateur peuvent savoir qu'une telle méthode existe et peut être appelée sur l'objet en question, même s'ils ne connaissent pas sa classe exacte. C'est une forme de polymorphisme qui est nécessaire avec le typage statique pour autoriser différentes classes d'implémentation tout en sachant qu'elles sont toutes légales.

Cela n'a pas vraiment de sens en Python, car il n'est pas typé statiquement. Vous n'avez pas besoin de déclarer la classe d'un objet, ni de convaincre le compilateur que les méthodes que vous appelez existent définitivement. Les "interfaces" dans un monde de type canard sont aussi simples que d'appeler la méthode et de s'assurer que l'objet peut gérer ce message de manière appropriée.

Remarque - les modifications apportées par des pythonistes plus expérimentés sont les bienvenues.

5
Andrzej Doyle

Peut-être que vous pouvez utiliser quelque chose comme ça. Cela agira comme une classe abstraite. Chaque sous-classe est donc obligée d'implémenter func1 ()

class Abstract:

    def func1(self):
        raise NotImplementedError("The method not implemented")
4
M S

J'ai écrit une bibliothèque en 3.5+ qui permet d'écrire des interfaces en Python.

L'essentiel est d'écrire un décorateur de classe à l'aide de inspect.

import inspect


def implements(interface_cls):
    def _decorator(cls):
        verify_methods(interface_cls, cls)
        verify_properties(interface_cls, cls)
        verify_attributes(interface_cls, cls)
        return cls

    return _decorator


def verify_methods(interface_cls, cls):
    methods_predicate = lambda m: inspect.isfunction(m) or inspect.ismethod(m)
    for name, method in inspect.getmembers(interface_cls, methods_predicate):
        signature = inspect.signature(method)
        cls_method = getattr(cls, name, None)
        cls_signature = inspect.signature(cls_method) if cls_method else None
        if cls_signature != signature:
            raise NotImplementedError(
                "'{}' must implement method '{}({})' defined in interface '{}'"
                .format(cls.__name__, name, signature, interface_cls.__name__)
            )


def verify_properties(interface_cls, cls):
    prop_attrs = dict(fget='getter', fset='setter', fdel='deleter')
    for name, prop in inspect.getmembers(interface_cls, inspect.isdatadescriptor):
        cls_prop = getattr(cls, name, None)
        for attr in prop_attrs:
            # instanceof doesn't work for class function comparison
            if type(getattr(prop, attr, None)) != type(getattr(cls_prop, attr, None)):
                raise NotImplementedError(
                    "'{}' must implement a {} for property '{}' defined in interface '{}'"  # flake8: noqa
                    .format(cls.__name__, prop_attrs[attr], name, interface_cls.__name__)
                )


def verify_attributes(interface_cls, cls):
    interface_attributes = get_attributes(interface_cls)
    cls_attributes = get_attributes(cls)
    for missing_attr in (interface_attributes - cls_attributes):
        raise NotImplementedError(
            "'{}' must have class attribute '{}' defined in interface '{}'"
            .format(cls.__name__, missing_attr, interface_cls.__name__)
        )


def get_attributes(cls):
    boring = dir(type('dummy', (object,), {}))
    return set(item[0] for item in inspect.getmembers(cls)
               if item[0] not in boring and not callable(item[1]))

Vous pouvez ensuite écrire des cours comme celui-ci:

class Quackable:
    def quack(self) -> bool:
        pass


@implements(Quackable)
class MallardDuck:    
    def quack(self) -> bool:
        pass

Ci-dessous vous donnerait cependant une erreur:

@implements(Quackable)
class RubberDuck:    
    def quack(self) -> str:
        pass

NotImplementedError: 'RubberdDuck' must implement method 'quack((self) -> bool)' defined in interface 'Quackable'
2
Kamil Sindi