In Python, come posso indicare che sto sostituendo un metodo?


173

In Java, ad esempio, l' @Overrideannotazione non solo fornisce il controllo in fase di compilazione di una sostituzione, ma rende eccellente il codice di auto-documentazione.

Sto solo cercando documentazione (anche se se è un indicatore per qualche pedina come il pylint, questo è un bonus). Posso aggiungere un commento o una docstring da qualche parte, ma qual è il modo idiomatico di indicare una sostituzione in Python?


13
In altre parole, non indichi mai che stai scavalcando un metodo? Lascia al lettore di capirlo da solo?
Bluu,

2
Sì, lo so che sembra una situazione soggetta a errore proveniente da un linguaggio compilato, ma devi solo accettarlo. In pratica, non ho trovato un grosso problema (Ruby nel mio caso, non Python, ma la stessa idea)
Ed S.

Certo, fatto. Sia la risposta di Triptych sia le risposte di mkorpela sono semplici, mi piace, ma lo spirito esplicito e troppo implicito di quest'ultimo vince e previene intelligentemente gli errori.
Bluu,

1
Non è direttamente lo stesso, ma le classi di base astratte controllano se tutti i metodi astratti sono stati sovrascritti da una sottoclasse. Naturalmente questo non aiuta se stai scavalcando metodi concreti.
Letmaik,

Risposte:


208

Sulla base di questa e della risposta di fwc: s ho creato un pacchetto installabile pip https://github.com/mkorpela/overrides

Di tanto in tanto finisco qui guardando questa domanda. Ciò accade principalmente dopo aver (di nuovo) visto lo stesso bug nella nostra base di codice: qualcuno ha dimenticato una classe di implementazione "interfaccia" durante la ridenominazione di un metodo in "interfaccia".

Bene Python non è Java ma Python ha il potere - ed esplicito è meglio che implicito - e ci sono casi concreti reali nel mondo reale in cui questa cosa mi avrebbe aiutato.

Quindi ecco uno schizzo di override decorator. Questo verificherà che la classe fornita come parametro abbia lo stesso nome di metodo (o qualcosa) del metodo che viene decorato.

Se riesci a pensare a una soluzione migliore, pubblicala qui!

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

Funziona come segue:

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


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

e se si esegue una versione difettosa genererà un errore di asserzione durante il caricamento della classe:

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

>> AssertionError!!!!!!!

18
Eccezionale. Questo ha rilevato un errore di ortografia la prima volta che l'ho provato. Complimenti.
Christopher Bruns,

7
mfbutner: non viene chiamato ogni volta che viene eseguito il metodo - solo quando viene creato il metodo.
mkorpela,

3
Questo è anche bello per le stringhe di documenti! overridespotrebbe copiare il docstring del metodo override se il metodo override non ne ha uno proprio.
Letmaik,

5
@mkorpela, Heh, questo tuo codice dovrebbe essere nel sistema lib predefinito di Python. Perché non lo metti nel sistema pip? : P

5
@mkorpela: Oh, e suggerisco di informare gli sviluppatori core di Python su questo pacchetto, potrebbero prendere in considerazione l'idea di aggiungere Decoratore Overide al sistema core Python. :)

30

Ecco un'implementazione che non richiede la specifica del nome 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

2
Un po 'magico ma semplifica notevolmente l'utilizzo tipico. Puoi includere esempi di utilizzo?
Bluu,

quali sono i costi medi e nel caso peggiore dell'utilizzo di questo decoratore, forse espressi come confronto con un decoratore incorporato come @classmethod o @property?
larham1,

4
@ larham1 Questo decoratore viene eseguito una volta, quando viene analizzata la definizione della classe, non per ogni chiamata. Pertanto, il suo costo di esecuzione è irrilevante rispetto al runtime del programma.
Abgan,

Questo sarà molto più bello in Python 3.6 grazie a PEP 487 .
Neil G,

Per ottenere un messaggio di errore migliore: asserire qualsiasi (hasattr (cls, method .__ name__) per cls in base_classes), il metodo Overriden "{}" non è stato trovato nella classe base. '. Format (method .__ name__)
Ivan Kovtun

14

Se lo desideri solo a scopo di documentazione, puoi definire il tuo decoratore override:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

Questo non è altro che eye-candy, a meno che non si crei l'override (f) in modo tale da verificare effettivamente l'override.

Ma poi, questo è Python, perché scriverlo come se fosse Java?


2
Si potrebbe aggiungere l'effettiva convalida tramite l'ispezione al overridedecoratore.
Erik Kaplun,

70
Ma poi, questo è Python, perché scriverlo come se fosse Java? Perché alcune idee in Java sono buone e vale la pena estenderle ad altre lingue?
Piotr Dobrogost,

9
Perché quando rinominate un metodo in una superclasse, sarebbe bello sapere che alcuni sottoclassi di 2 livelli in basso lo stavano scavalcando. Certo, è facile da controllare, ma un piccolo aiuto dal parser di lingua non farebbe male.
Abgan

4
Perché è una buona idea. Il fatto che una varietà di altre lingue abbia questa caratteristica non è un argomento, né a favore né contro.
sfkleach,

6

Python non è Java. Ovviamente non esiste nulla di simile al controllo in fase di compilazione.

Penso che un commento nella documentazione sia abbondante. Ciò consente a qualsiasi utente del metodo di digitare help(obj.method)e vedere che il metodo è una sostituzione.

Puoi anche estendere esplicitamente un'interfaccia con class Foo(Interface), che consentirà agli utenti di digitare help(Interface.method)per farsi un'idea delle funzionalità che il tuo metodo è destinato a fornire.


57
Il vero punto di @OverrideJava non è documentare, ma cogliere un errore quando si intendeva sovrascrivere un metodo, ma alla fine ne è stato definito uno nuovo (ad es. Perché si è sbagliato a scrivere un nome; in Java, può accadere anche perché si è usato la firma sbagliata, ma questo non è un problema in Python - ma l'errore di ortografia lo è ancora).
Pavel Minaev,

2
@ Pavel Minaev: Vero, ma è comunque conveniente avere per la documentazione, specialmente se stai usando un IDE / editor di testo che non ha indicatori automatici per le sostituzioni (il JDT di Eclipse le mostra ordinatamente accanto ai numeri di riga, per esempio).
Tuukka Mustonen,

2
@PavelMinaev Wrong. Uno dei punti principali di @Overrideè la documentazione oltre al controllo del tempo di compilazione.
Siamii,

6
@siamii Penso che un aiuto alla documentazione sia eccezionale, ma in tutta la documentazione ufficiale Java che vedo, indicano solo l'importanza dei controlli del tempo di compilazione. Per favore, conferma la tua affermazione che Pavel è "sbagliato".
Andrew Mellinger,

5

Improvvisando su @mkorpela un'ottima risposta , ecco una versione con

controlli, nomi e oggetti di errore più precisi

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


Ecco come appare in pratica:

NotImplementedError" non implementato nella classe base "

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

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

provoca un NotImplementedErrorerrore più descrittivo

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

pila completa

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" tipo implementato previsto "

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

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

provoca un NotImplementedErrorerrore più descrittivo

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

pila completa

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




La cosa grandiosa della risposta di @mkorpela è che il controllo avviene durante una fase di inizializzazione. Non è necessario che il controllo sia "eseguito". Facendo riferimento agli esempi precedenti, class Bnon viene mai inizializzato ( B()) e tuttavia NotImplementedErrorverrà comunque generato. Ciò significa che gli overrideserrori vengono rilevati prima.


Hey! Questo sembra interessante. Potresti prendere in considerazione una richiesta pull sul mio progetto ipromise? Ho aggiunto una risposta
Neil G

@NeilG Ho modificato il progetto ipromise e ho codificato un po '. Sembra che tu l'abbia sostanzialmente implementato all'interno overrides.py. Non sono sicuro di cos'altro posso migliorare significativamente se non per cambiare i tipi di eccezione da TypeErrora NotImplementedError.
JamesThomasMoon1979

Hey! Grazie, non ho controlli che l'oggetto sovrascritto abbia effettivamente il tipo types.MethodType. Questa è stata una buona idea nella tua risposta.
Neil G

2

Come altri hanno già detto a differenza di Java, non esiste il tag @Overide, tuttavia sopra puoi crearne uno tuo usando decoratori, tuttavia suggerirei di usare il metodo globale getattrib () invece di usare il dict interno in modo da ottenere qualcosa di simile al seguente:

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

Se lo si desidera, è possibile catturare getattr () nel proprio tentativo, catturare aumentare il proprio errore, ma penso che il metodo getattr sia migliore in questo caso.

Inoltre, vengono catturati tutti gli elementi associati a una classe, inclusi i metodi di classe e i vendibili


2

Sulla base della grande risposta di @ mkorpela, ho scritto un pacchetto simile ( ipromise pypi github ) che esegue molti più controlli:

Supponiamo che Aerediti da Be C, Berediti da C.

Il modulo ipromise verifica che:

  • Se A.fle sostituzioni B.f, B.fdeve esistere, e Adeve ereditare da B. (Questo è il controllo dal pacchetto di sostituzione).

  • Non hai il modello A.fdichiara che ha la precedenza B.f, che quindi dichiara che ha la precedenza C.f. Adovrebbe dire che ha la precedenza da C.fpoiché Bpotrebbe decidere di interrompere la sostituzione di questo metodo e ciò non dovrebbe comportare aggiornamenti a valle.

  • Non hai il modello A.fdichiara che ha la precedenza C.f, ma B.fnon dichiara la sua sostituzione.

  • Non hai il modello A.fdichiara che ha la precedenza C.f, ma B.fdichiara che ha la precedenza su alcuni D.f.

Ha anche varie funzionalità per contrassegnare e verificare l'implementazione di un metodo astratto.


0

Hear è il più semplice e funziona sotto Jython con le classi 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()

0

Non solo il decoratore che ho verificato ha verificato se il nome dell'attributo prevalente è una superclasse della classe in cui si trova l'attributo senza dover specificare una superclasse, ma ha anche verificato che l'attributo prevalente sia dello stesso tipo del sovrascritto attributo. I metodi di classe sono trattati come metodi e i metodi statici sono trattati come funzioni. Questo decoratore funziona per callable, metodi di classe, metodi statici e proprietà.

Per il codice sorgente, consultare: https://github.com/fireuser909/override

Questo decoratore funziona solo per le classi che sono istanze di override.OverridesMeta ma se la tua classe è un'istanza di una metaclasse personalizzata usa la funzione create_custom_overrides_meta per creare una metaclasse compatibile con il decoratore di override. Per i test, eseguire il modulo di sostituzione .__ init__.


0

In Python 2.6+ e Python 3.2+ è possibile farlo (in realtà simularlo , Python non supporta il sovraccarico delle funzioni e la classe figlio sostituisce automaticamente il metodo parent). Per questo possiamo usare Decoratori. Ma prima, nota che Python @decoratorse Java @Annotationssono cose totalmente diverse. Il precedente è un wrapper con codice concreto mentre in seguito uno è un flag per il compilatore.

Per questo, prima di tutto 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)

produzione:

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

Nota che devi usare decoratore qui tra parentesi

Una cosa da ricordare è che dal momento che Python non ha il sovraccarico delle funzioni direttamente, quindi anche se la Classe B non eredita dalla Classe A ma ha bisogno di tutte quelle foos di quelle che devi usare anche @Override (anche se l'uso dell'alias 'Overload' sembrerà meglio in quel caso)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.