Wie gebe ich in Python an, dass ich eine Methode überschreibe?

173

In Java bietet die @OverrideAnnotation beispielsweise nicht nur eine Überprüfung einer Überschreibung zur Kompilierungszeit, sondern sorgt auch für einen hervorragenden selbstdokumentierenden Code.

Ich suche nur nach Dokumentation (obwohl es ein Bonus ist, wenn es ein Indikator für einen Prüfer wie Pylint ist). Ich kann irgendwo einen Kommentar oder eine Dokumentzeichenfolge hinzufügen, aber wie kann eine Überschreibung in Python idiomatisch angezeigt werden?

Bluu
quelle
13
Mit anderen Worten, Sie geben nie an, dass Sie eine Methode überschreiben? Überlassen Sie es dem Leser, das selbst herauszufinden?
Bluu
2
Ja, ich weiß, es scheint eine fehleranfällige Situation zu sein, die von einer kompilierten Sprache herrührt, aber Sie müssen sie einfach akzeptieren. In der Praxis habe ich nicht festgestellt, dass es ein großes Problem ist (Ruby in meinem Fall, nicht Python, aber die gleiche Idee)
Ed S.
Sicher, fertig. Sowohl die Antwort von Triptychon als auch die von mkorpela sind einfach, das gefällt mir, aber der explizit über implizite Geist des letzteren und die verständliche Verhinderung von Fehlern gewinnen.
Bluu
1
Es ist nicht direkt dasselbe, aber abstrakte Basisklassen prüfen, ob alle abstrakten Methoden von einer Unterklasse überschrieben wurden. Dies hilft natürlich nicht, wenn Sie konkrete Methoden überschreiben.
Letmaik

Antworten:

205

Basierend auf dieser und der Antwort von fwc: s habe ich ein installierbares Pip-Paket https://github.com/mkorpela/overrides erstellt

Von Zeit zu Zeit schaue ich mir diese Frage an. Dies geschieht hauptsächlich, nachdem (erneut) derselbe Fehler in unserer Codebasis aufgetreten ist: Jemand hat beim Umbenennen einer Methode in der "Schnittstelle" eine Implementierungsklasse "interface" vergessen.

Nun, Python ist nicht Java, aber Python hat Macht - und explizit ist besser als implizit - und es gibt echte konkrete Fälle in der realen Welt, in denen mir dieses Ding geholfen hätte.

Hier ist eine Skizze des Overrides-Dekorateurs. Dadurch wird überprüft, ob die als Parameter angegebene Klasse denselben Methodennamen (oder einen anderen Namen) wie die zu dekorierende Methode hat.

Wenn Sie sich eine bessere Lösung vorstellen können, posten Sie diese bitte hier!

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

Es funktioniert wie folgt:

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


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

und wenn Sie eine fehlerhafte Version ausführen, wird beim Laden der Klasse ein Assertionsfehler ausgelöst:

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

>> AssertionError!!!!!!!
mkorpela
quelle
17
Genial. Dies hat beim ersten Versuch einen Rechtschreibfehler verursacht. Ein großes Lob.
Christopher Bruns
7
mfbutner: Es wird nicht jedes Mal aufgerufen, wenn die Methode ausgeführt wird - nur wenn die Methode erstellt wird.
Mkorpela
3
Dies ist auch schön für Doc-Strings! overrideskönnte die Dokumentzeichenfolge der überschriebenen Methode kopieren, wenn die überschreibende Methode keine eigene hat.
Letmaik
5
@mkorpela, Heh, dein Code sollte im Python-Standard-Lib-System sein. Warum setzen Sie dies nicht in Pip-System? : P
5
@mkorpela: Oh, und ich schlage vor, die Python-Kernentwickler über dieses Paket zu informieren. Vielleicht möchten sie darüber nachdenken, dem Kern-Python-System Overide Decorator hinzuzufügen. :)
30

Hier ist eine Implementierung, für die keine Angabe des Namens der Schnittstellenklasse erforderlich ist.

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
quelle
2
Ein bisschen magisch, macht aber den typischen Gebrauch viel einfacher. Können Sie Anwendungsbeispiele hinzufügen?
Bluu
Was sind die durchschnittlichen und Worst-Case-Kosten für die Verwendung dieses Dekorators, möglicherweise ausgedrückt als Vergleich mit einem eingebauten Dekorator wie @classmethod oder @property?
Larham1
4
@ larham1 Dieser Dekorator wird einmal ausgeführt, wenn die Klassendefinition analysiert wird, nicht bei jedem Aufruf. Daher sind die Ausführungskosten im Vergleich zur Programmlaufzeit irrelevant.
Abgan
Dies wird in Python 3.6 dank PEP 487 viel besser .
Neil G
Um eine bessere Fehlermeldung zu erhalten: Aktivieren Sie any (hasattr (cls, Methode .__ name__) für cls in base_classes), 'Overriden-Methode "{}" wurde in der Basisklasse nicht gefunden.'. Format (Methode .__ name__)
Ivan Kovtun
14

Wenn Sie dies nur zu Dokumentationszwecken wünschen, können Sie Ihren eigenen Override-Dekorator definieren:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

Dies ist wirklich nichts anderes als eine Augenweide, es sei denn, Sie erstellen Override (f) so, dass tatsächlich nach einem Override gesucht wird.

Aber das ist Python. Warum sollte man es so schreiben, als wäre es Java?

Ber
quelle
2
Man könnte dem overrideDekorateur eine tatsächliche Validierung durch Inspektion hinzufügen .
Erik Kaplun
69
Aber das ist Python. Warum sollte man es so schreiben, als wäre es Java? Weil einige Ideen in Java gut sind und es wert sind, auf andere Sprachen ausgedehnt zu werden?
Piotr Dobrogost
9
Denn wenn Sie eine Methode in einer Oberklasse umbenennen, wäre es schön zu wissen, dass einige Ebenen der Unterklasse 2 diese überschrieben haben. Sicher, es ist leicht zu überprüfen, aber ein bisschen Hilfe vom Sprachparser würde nicht schaden.
Abgan
4
Weil es eine gute Idee ist. Die Tatsache, dass eine Vielzahl anderer Sprachen über diese Funktion verfügen, ist kein Argument - weder dafür noch dagegen.
Sfkleach
6

Python ist kein Java. Es gibt natürlich keine Überprüfung der Kompilierungszeit.

Ich denke, ein Kommentar in der Dokumentationszeichenfolge ist ausreichend. Auf diese Weise kann jeder Benutzer Ihrer Methode eingeben help(obj.method)und feststellen, dass es sich bei der Methode um eine Überschreibung handelt.

Sie können auch eine Schnittstelle explizit erweitern, mit class Foo(Interface)der Benutzer tippen können help(Interface.method), um sich ein Bild von der Funktionalität zu machen, die Ihre Methode bereitstellen soll.

Triptychon
quelle
56
Der eigentliche Sinn von @OverrideJava besteht nicht darin, zu dokumentieren, sondern einen Fehler zu erkennen, wenn Sie beabsichtigten, eine Methode zu überschreiben, aber am Ende eine neue zu definieren (z. B. weil Sie einen Namen falsch geschrieben haben; in Java kann dies auch passieren, weil Sie ihn verwendet haben die falsche Signatur, aber dies ist kein Problem in Python - aber Rechtschreibfehler sind immer noch).
Pavel Minaev
2
@ Pavel Minaev: Stimmt, aber es ist immer noch praktisch, eine Dokumentation zu haben, insbesondere wenn Sie einen IDE- / Texteditor verwenden, der keine automatischen Anzeigen für Überschreibungen enthält (Eclipse's JDT zeigt sie beispielsweise ordentlich neben Zeilennummern an).
Tuukka Mustonen
2
@ PavelMinaev Falsch. Einer der Hauptpunkte @Overrideist neben der Überprüfung der Kompilierungszeit auch die Dokumentation.
Siamii
6
@siamii Ich denke, eine Hilfe zur Dokumentation ist großartig, aber in allen offiziellen Java-Dokumentationen, die ich sehe, zeigen sie nur die Wichtigkeit der Überprüfungen der Kompilierungszeit an. Bitte begründen Sie Ihre Behauptung, dass Pavel "falsch" ist.
Andrew Mellinger
5

Improvisieren auf @mkorpela tolle Antwort , hier ist eine Version mit

genauere Überprüfungen, Benennungen und ausgelöste Fehlerobjekte

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


So sieht es in der Praxis aus:

NotImplementedError" nicht in Basisklasse implementiert "

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

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

führt zu einem aussagekräftigeren NotImplementedErrorFehler

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

voller Stapel

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" erwarteter implementierter Typ "

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

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

führt zu einem aussagekräftigeren NotImplementedErrorFehler

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

voller Stapel

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'>




Das Tolle an der Antwort von @mkorpela ist, dass die Überprüfung während einer Initialisierungsphase erfolgt. Die Prüfung muss nicht "ausgeführt" werden. In Bezug auf die vorherigen Beispiele class Bwird nie initialisiert ( B()), aber der NotImplementedErrorWille wird immer noch ausgelöst. Dies bedeutet, dass overridesFehler früher erkannt werden.

JamesThomasMoon1979
quelle
Hallo! Das sieht interessant aus. Könnten Sie eine Pull-Anfrage für mein ipromise-Projekt stellen? Ich habe eine Antwort hinzugefügt.
Neil G
@NeilG Ich habe das ipromise- Projekt gegabelt und ein bisschen codiert. Es scheint, als hätten Sie dies im Wesentlichen innerhalb implementiert overrides.py. Ich bin mir nicht sicher, was ich sonst noch erheblich verbessern kann, außer die Ausnahmetypen von TypeErrorauf zu ändern NotImplementedError.
JamesThomasMoon1979
Hallo! Vielen Dank, ich habe keine Überprüfung, ob das überschriebene Objekt tatsächlich einen Typ hat types.MethodType. Das war eine gute Idee in Ihrer Antwort.
Neil G
2

Wie andere bereits gesagt haben, gibt es im Gegensatz zu Java kein @ Overde-Tag. Oben können Sie jedoch mithilfe von Dekoratoren ein eigenes Tag erstellen. Ich würde jedoch empfehlen, die globale Methode getattrib () anstelle des internen Diktats zu verwenden, damit Sie Folgendes erhalten:

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

Wenn Sie möchten, können Sie getattr () in Ihrem eigenen Versuch abfangen. Catch löst Ihren eigenen Fehler aus, aber ich denke, die Methode getattr ist in diesem Fall besser.

Außerdem werden alle an eine Klasse gebundenen Elemente erfasst, einschließlich Klassenmethoden und Vairables

Streit x 2
quelle
2

Basierend auf der großartigen Antwort von @ mkorpela habe ich ein ähnliches Paket ( ipromise pypi github ) geschrieben, das viele weitere Überprüfungen durchführt:

Angenommen , Aerbt von Bund C, Berbt von C.

Das Modul ipromise überprüft Folgendes :

  • Wenn A.füberschreibt B.f, B.fmuss vorhanden sein und von Aerben B. (Dies ist die Prüfung aus dem Overrides-Paket).

  • Sie haben nicht das Muster A.fdeklariert, dass es überschreibt B.f, was dann deklariert, dass es überschreibt C.f. Asollte sagen, dass es überschreibt von C.fda Bkönnte entscheiden, diese Methode nicht mehr zu überschreiben, und das sollte nicht zu nachgelagerten Updates führen.

  • Das Muster A.fdeklariert nicht, dass es überschrieben wird C.f, B.fdeklariert jedoch nicht, dass es überschrieben wird .

  • Sie haben nicht das Muster A.fdeklariert, dass es überschreibt C.f, aber B.fdeklariert, dass es von einigen überschreibt D.f.

Es verfügt auch über verschiedene Funktionen zum Markieren und Überprüfen der Implementierung einer abstrakten Methode.

Neil G.
quelle
0

Hören ist am einfachsten und funktioniert unter Jython mit Java-Klassen:

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()
user3034016
quelle
0

Der Dekorateur, den ich erstellt habe, hat nicht nur überprüft, ob der Name des überschreibenden Attributs in einer Oberklasse der Klasse ist, in der sich das Attribut befindet, ohne dass eine Oberklasse angegeben werden muss, sondern auch, ob das überschreibende Attribut vom selben Typ wie das überschriebene sein muss Attribut. Klassenmethoden werden wie Methoden behandelt und statische Methoden werden wie Funktionen behandelt. Dieser Dekorator arbeitet für Callables, Klassenmethoden, statische Methoden und Eigenschaften.

Quellcode finden Sie unter: https://github.com/fireuser909/override

Dieser Dekorator funktioniert nur für Klassen, die Instanzen von override.OverridesMeta sind. Wenn Ihre Klasse jedoch eine Instanz einer benutzerdefinierten Metaklasse ist, verwenden Sie die Funktion create_custom_overrides_meta, um eine Metaklasse zu erstellen, die mit dem Überschreibungsdekorator kompatibel ist. Führen Sie für Tests das Modul override .__ init__ aus.

Michael
quelle
0

In Python 2.6+ und Python 3.2+ können Sie dies tun ( simulieren Sie es tatsächlich , Python unterstützt keine Funktionsüberladung und die untergeordnete Klasse überschreibt automatisch die Methode der Eltern). Wir können hierfür Dekorateure verwenden. Beachten Sie jedoch zunächst, dass Pythons @decoratorsund Java @Annotationsvöllig unterschiedliche Dinge sind. Der vorherige ist ein Wrapper mit konkretem Code, während der spätere ein Flag für den Compiler ist.

Dazu zuerst 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)

Ausgabe:

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

Beachten Sie, dass Sie hier den Dekorator mit Klammern verwenden müssen

Eine Sache, an die Sie sich erinnern sollten, ist, dass Python keine direkte Funktionsüberladung hat. Selbst wenn Klasse B nicht von Klasse A erbt, sondern alle diese foobenötigt, müssen Sie auch @Override verwenden (obwohl der Alias ​​'Überladung' aussehen wird besser in diesem Fall)

mradul
quelle