En Python, comment indiquer que je remplace une méthode?

173

En Java, par exemple, l' @Overrideannotation fournit non seulement une vérification à la compilation d'un remplacement, mais constitue un excellent code d'auto-documentation.

Je cherche juste de la documentation (même si c'est un indicateur pour un vérificateur comme pylint, c'est un bonus). Je peux ajouter un commentaire ou une docstring quelque part, mais quelle est la manière idiomatique d'indiquer un remplacement en Python?

Bluu
la source
13
En d'autres termes, vous n'indiquez jamais que vous remplacez une méthode? Laisser au lecteur le soin de comprendre cela lui-même?
Bluu
2
Ouais, je sais que cela ressemble à une situation sujette aux erreurs provenant d'un langage compilé, mais il suffit de l'accepter. En pratique, je n'ai pas trouvé que ce soit vraiment un problème (Ruby dans mon cas, pas Python, mais même idée)
Ed S.
Bien sûr, c'est fait. La réponse de Triptych et les réponses de mkorpela sont simples, j'aime ça, mais l'esprit explicite sur-implicite de ce dernier et la prévention intelligible des erreurs l'emportent.
Bluu
1
Ce n'est pas directement la même chose, mais les classes de base abstraites vérifient si toutes les méthodes abstraites ont été remplacées par une sous-classe. Bien sûr, cela n'aide pas si vous remplacez les méthodes concrètes.
letmaik

Réponses:

208

Sur la base de cela et de la réponse de fwc: s, j'ai créé un package installable pip https://github.com/mkorpela/overrides

De temps en temps, je finis par examiner cette question. Cela se produit principalement après avoir (à nouveau) vu le même bogue dans notre base de code: quelqu'un a oublié une classe d'implémentation "interface" en renommant une méthode dans "interface".

Eh bien, Python n'est pas Java mais Python a du pouvoir - et l'explicite est mieux qu'implicite - et il y a de vrais cas concrets dans le monde réel où cela m'aurait aidé.

Voici donc un croquis du décorateur de substitutions. Cela vérifiera que la classe donnée en paramètre a le même nom de méthode (ou quelque chose) que la méthode en cours de décoration.

Si vous pouvez penser à une meilleure solution, veuillez la poster ici!

def overrides(interface_class):
    def overrider(method):
        assert(method.__name__ in dir(interface_class))
        return method
    return overrider

Cela fonctionne comme suit:

class MySuperInterface(object):
    def my_method(self):
        print 'hello world!'


class ConcreteImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def my_method(self):
        print 'hello kitty!'

et si vous faites une version défectueuse, cela déclenchera une erreur d'assertion lors du chargement de la classe:

class ConcreteFaultyImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def your_method(self):
        print 'bye bye!'

>> AssertionError!!!!!!!
mkorpela
la source
18
Impressionnant. Cela a attrapé un bug d'orthographe la première fois que je l'ai essayé. Gloire.
Christopher Bruns
7
mfbutner: il n'est pas appelé à chaque fois que la méthode est exécutée - uniquement lorsque la méthode est créée.
mkorpela
3
C'est aussi bien pour les chaînes doc! overridespourrait copier la docstring de la méthode surchargée si la méthode surchargée n'en a pas une.
letmaik
5
@mkorpela, Heh ce code devrait être dans le système de bibliothèque par défaut de python. Pourquoi ne mettez-vous pas cela dans le système pip? : P
5
@mkorpela: Oh et je suggère d'informer les développeurs du noyau python de ce paquet, ils voudront peut-être envisager d'ajouter un décorateur de remplacement au système python principal. :)
30

Voici une implémentation qui ne nécessite pas la spécification du nom interface_class.

import inspect
import re

def overrides(method):
    # actually can't do this because a method is really just a function while inside a class def'n  
    #assert(inspect.ismethod(method))

    stack = inspect.stack()
    base_classes = re.search(r'class.+\((.+)\)\s*\:', stack[2][4][0]).group(1)

    # handle multiple inheritance
    base_classes = [s.strip() for s in base_classes.split(',')]
    if not base_classes:
        raise ValueError('overrides decorator: unable to determine base class') 

    # stack[0]=overrides, stack[1]=inside class def'n, stack[2]=outside class def'n
    derived_class_locals = stack[2][0].f_locals

    # replace each class name in base_classes with the actual class type
    for i, base_class in enumerate(base_classes):

        if '.' not in base_class:
            base_classes[i] = derived_class_locals[base_class]

        else:
            components = base_class.split('.')

            # obj is either a module or a class
            obj = derived_class_locals[components[0]]

            for c in components[1:]:
                assert(inspect.ismodule(obj) or inspect.isclass(obj))
                obj = getattr(obj, c)

            base_classes[i] = obj


    assert( any( hasattr(cls, method.__name__) for cls in base_classes ) )
    return method
fwc
la source
2
Un peu magique mais rend l'utilisation typique beaucoup plus facile. Pouvez-vous inclure des exemples d'utilisation?
Bluu
Quels sont les coûts moyens et les pires cas d'utilisation de ce décorateur, peut-être exprimés en comparaison avec un décorateur intégré comme @classmethod ou @property?
larham1
4
@ larham1 Ce décorateur est exécuté une fois, lorsque la définition de classe est analysée, pas à chaque appel. Par conséquent, son coût d'exécution n'est pas pertinent par rapport à l'exécution du programme.
Abgan
Ce sera beaucoup plus agréable en Python 3.6 grâce à PEP 487 .
Neil G
Pour obtenir un meilleur message d'erreur: assert any (hasattr (cls, method .__ name__) for cls in base_classes), 'Overriden method "{}" was not found in the base class.'. Format (method .__ name__)
Ivan Kovtun
14

Si vous le souhaitez uniquement à des fins de documentation, vous pouvez définir votre propre décorateur de remplacement:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

Ce n'est vraiment rien d'autre que du plaisir pour les yeux, à moins que vous ne créiez un remplacement (f) de manière à vérifier en fait un remplacement.

Mais alors, c'est Python, pourquoi l'écrire comme si c'était Java?

Ber
la source
2
On pourrait ajouter une validation réelle via l'inspection au overridedécorateur.
Erik Kaplun le
70
Mais alors, c'est Python, pourquoi l'écrire comme si c'était Java? Parce que certaines idées en Java sont bonnes et valent la peine d'être étendues à d'autres langages?
Piotr Dobrogost
9
Parce que lorsque vous renommez une méthode dans une superclasse, il serait bon de savoir que certaines sous-classes 2 niveaux inférieurs la remplaçaient. Bien sûr, c'est facile à vérifier, mais un peu d'aide de l'analyseur de langage ne ferait pas de mal.
Abgan
4
Parce que c'est une bonne idée. Le fait qu'une variété d'autres langages aient cette fonctionnalité n'est pas un argument - ni pour ni contre.
sfkleach
6

Python n'est pas Java. Il n'y a bien sûr pas vraiment de vérification à la compilation.

Je pense qu'un commentaire dans la docstring est suffisant. Cela permet à tout utilisateur de votre méthode de taper help(obj.method)et de voir que la méthode est une substitution.

Vous pouvez également étendre explicitement une interface avec class Foo(Interface), ce qui permettra aux utilisateurs de taper help(Interface.method)pour avoir une idée de la fonctionnalité que votre méthode est censée fournir.

Triptyque
la source
57
Le vrai point de @OverrideJava n'est pas de documenter - c'est d'attraper une erreur lorsque vous aviez l'intention de remplacer une méthode, mais que vous avez fini par en définir une nouvelle (par exemple parce que vous avez mal orthographié un nom; en Java, cela peut aussi arriver parce que vous avez utilisé la mauvaise signature, mais ce n'est pas un problème en Python - mais une faute d'orthographe l'est toujours).
Pavel Minaev
2
@ Pavel Minaev: C'est vrai, mais c'est toujours pratique d'avoir pour la documentation, surtout si vous utilisez un IDE / éditeur de texte qui n'a pas d'indicateurs automatiques pour les remplacements (le JDT d'Eclipse les montre parfaitement à côté des numéros de ligne, par exemple).
Tuukka Mustonen
2
@PavelMinaev Faux. L'un des principaux points de @Overrideest la documentation en plus de la vérification du temps de compilation.
siamii
6
@siamii Je pense qu'une aide à la documentation est excellente, mais dans toute la documentation officielle Java que je vois, elles n'indiquent que l'importance des contrôles de temps de compilation. Veuillez justifier votre affirmation selon laquelle Pavel a «tort».
Andrew Mellinger
5

Improviser sur @mkorpela bonne réponse , voici une version avec

vérifications, dénomination et objets d'erreur plus précis

def overrides(interface_class):
    """
    Function override annotation.
    Corollary to @abc.abstractmethod where the override is not of an
    abstractmethod.
    Modified from answer https://stackoverflow.com/a/8313042/471376
    """
    def confirm_override(method):
        if method.__name__ not in dir(interface_class):
            raise NotImplementedError('function "%s" is an @override but that'
                                      ' function is not implemented in base'
                                      ' class %s'
                                      % (method.__name__,
                                         interface_class)
                                      )

        def func():
            pass

        attr = getattr(interface_class, method.__name__)
        if type(attr) is not type(func):
            raise NotImplementedError('function "%s" is an @override'
                                      ' but that is implemented as type %s'
                                      ' in base class %s, expected implemented'
                                      ' type %s'
                                      % (method.__name__,
                                         type(attr),
                                         interface_class,
                                         type(func))
                                      )
        return method
    return confirm_override


Voici à quoi cela ressemble en pratique:

NotImplementedError" non implémenté dans la classe de base "

class A(object):
    # ERROR: `a` is not a implemented!
    pass

class B(A):
    @overrides(A)
    def a(self):
        pass

entraîne une NotImplementedErrorerreur plus descriptive

function "a" is an @override but that function is not implemented in base class <class '__main__.A'>

un paquet entier

Traceback (most recent call last):
  
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 110, in confirm_override
    interface_class)
NotImplementedError: function "a" is an @override but that function is not implemented in base class <class '__main__.A'>


NotImplementedError" type d'implémentation attendu "

class A(object):
    # ERROR: `a` is not a function!
    a = ''

class B(A):
    @overrides(A)
    def a(self):
        pass

entraîne une NotImplementedErrorerreur plus descriptive

function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>

un paquet entier

Traceback (most recent call last):
  
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 125, in confirm_override
    type(func))
NotImplementedError: function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>




Le grand avantage de la réponse @mkorpela est que la vérification se produit pendant une phase d'initialisation. Le contrôle n'a pas besoin d'être "exécuté". En se référant aux exemples précédents, class Bn'est jamais initialisé ( B()) mais le NotImplementedErrorsera quand même augmenté. Cela signifie que les overrideserreurs sont détectées plus tôt.

JamesThomasMoon1979
la source
Hey! Cela semble intéressant. Pourriez-vous envisager de faire une pull request sur mon projet ipromise? J'ai ajouté une réponse.
Neil G
@NeilG J'ai bifurqué le projet ipromise et codé un peu. Il semble que vous ayez essentiellement mis en œuvre cela à l'intérieur overrides.py. Je ne suis pas sûr de ce que je peux améliorer de manière significative, sauf pour changer les types d'exceptions de TypeErrorà NotImplementedError.
JamesThomasMoon1979
Hey! Merci, je n'ai pas de vérification que l'objet surchargé a réellement un type types.MethodType. C'était une bonne idée dans votre réponse.
Neil G
2

Comme d'autres l'ont dit, contrairement à Java, il n'y a pas de balise @Overide, mais ci-dessus, vous pouvez créer les vôtres à l'aide de décorateurs, mais je suggérerais d'utiliser la méthode globale getattrib () au lieu d'utiliser le dict interne afin d'obtenir quelque chose comme ce qui suit:

def Override(superClass):
    def method(func)
        getattr(superClass,method.__name__)
    return method

Si vous le vouliez, vous pourriez attraper getattr () dans votre propre try catch, mais je pense que la méthode getattr est meilleure dans ce cas.

Cela attrape également tous les éléments liés à une classe, y compris les méthodes de classe et les vairables

Bicker x 2
la source
2

Sur la base de la bonne réponse de @ mkorpela, j'ai écrit un package similaire ( ipromise pypi github ) qui effectue beaucoup plus de vérifications:

Supposons Ahérite de Bet C, Bhérite de C.

Le module ipromise vérifie que:

  • Si A.foverrides B.f, B.fdoit exister et Adoit hériter de B. (Ceci est la vérification du package des remplacements).

  • Vous n'avez pas le modèle A.fdéclare qu'il remplace B.f, qui déclare ensuite qu'il remplace C.f. Adevrait dire qu'il remplace de C.fpuisque Bpourrait décider d'arrêter de remplacer cette méthode, et cela ne devrait pas entraîner de mises à jour en aval.

  • Vous n'avez pas le modèle A.fdéclare qu'il remplace C.f, mais B.fne déclare pas son remplacement.

  • Vous n'avez pas le modèle A.fdéclare qu'il remplace C.f, mais B.fdéclare qu'il remplace certains D.f.

Il possède également diverses fonctionnalités de marquage et de vérification mettant en œuvre une méthode abstraite.

Neil G
la source
0

Hear est le plus simple et fonctionne sous Jython avec des classes Java:

class MyClass(SomeJavaClass):
     def __init__(self):
         setattr(self, "name_of_method_to_override", __method_override__)

     def __method_override__(self, some_args):
         some_thing_to_do()
utilisateur3034016
la source
0

Non seulement le décorateur que j'ai fait vérifier si le nom de l'attribut de remplacement dans est une superclasse de la classe dans laquelle l'attribut se trouve sans avoir à spécifier une superclasse, ce décorateur vérifie également pour s'assurer que l'attribut de remplacement doit être du même type que l'attribut remplacé attribut. Les méthodes de classe sont traitées comme des méthodes et les méthodes statiques sont traitées comme des fonctions. Ce décorateur fonctionne pour les callables, les méthodes de classe, les méthodes statiques et les propriétés.

Pour le code source, voir: https://github.com/fireuser909/override

Ce décorateur ne fonctionne que pour les classes qui sont des instances de remplacement.OverridesMeta mais si votre classe est une instance d'une métaclasse personnalisée, utilisez la fonction create_custom_overrides_meta pour créer une métaclasse compatible avec le décorateur de remplacement. Pour les tests, exécutez le module override .__ init__.

Michael
la source
0

En Python 2.6+ et Python 3.2+, vous pouvez le faire (en fait , simulez-le , Python ne prend pas en charge la surcharge de fonctions et la classe enfant remplace automatiquement la méthode parent). Nous pouvons utiliser des décorateurs pour cela. Mais d'abord, notez que Python @decoratorset Java @Annotationssont des choses totalement différentes. Le précédent est un wrapper avec du code concret tandis que le dernier est un indicateur à compilateur.

Pour cela, faites d'abord pip install multipledispatch

from multipledispatch import dispatch as Override
# using alias 'Override' just to give you some feel :)

class A:
    def foo(self):
        print('foo in A')

    # More methods here


class B(A):
    @Override()
    def foo(self):
        print('foo in B')
    
    @Override(int)
    def foo(self,a):
        print('foo in B; arg =',a)
        
    @Override(str,float)
    def foo(self,a,b):
        print('foo in B; arg =',(a,b))
        
a=A()
b=B()
a.foo()
b.foo()
b.foo(4)
b.foo('Wheee',3.14)

production:

foo in A
foo in B
foo in B; arg = 4
foo in B; arg = ('Wheee', 3.14)

Notez que vous devez utiliser le décorateur ici avec des parenthèses

Une chose à retenir est que puisque Python n'a pas de surcharge de fonction directement, donc même si la classe B n'hérite pas de la classe A mais a besoin de tous ces foos, vous devez également utiliser @Override (bien que l'utilisation de l'alias 'Overload' semble mieux dans ce cas)

mradul
la source