Jak w Pythonie mam wskazać, że nadpisuję metodę?

173

Na przykład w Javie @Overrideadnotacja nie tylko zapewnia sprawdzanie zastąpienia w czasie kompilacji, ale także stanowi doskonały samodokumentujący się kod.

Szukam tylko dokumentacji (chociaż jeśli jest to wskaźnik do jakiegoś kratownicy jak pylint, to jest to bonus). Mogę gdzieś dodać komentarz lub dokument, ale jaki jest idiomatyczny sposób wskazania zastąpienia w Pythonie?

Bluu
źródło
13
Innymi słowy, nigdy nie wskazałeś, że zastępujesz metodę? Zostawić czytelnikowi samemu to rozgryźć?
Bluu
2
Tak, wiem, że wygląda to na podatną na błędy sytuację pochodzącą ze skompilowanego języka, ale musisz to zaakceptować. W praktyce nie okazało się być wielkim problemem (Ruby w moim przypadku, nie Pythona, ale sam pomysł)
Ed S.
Jasne, gotowe. Zarówno odpowiedź Tryptyku, jak i odpowiedzi mkorpeli są proste, podoba mi się to, ale ten drugi wyraźnie i nadmiernie ukryty duch i inteligentnie zapobiegający błędom wygrywa.
Bluu
1
Nie jest to bezpośrednio to samo, ale abstrakcyjne klasy bazowe sprawdzają, czy wszystkie metody abstrakcyjne zostały zastąpione przez podklasę. Oczywiście to nie pomaga, jeśli ignorujesz konkretne metody.
letmaik

Odpowiedzi:

206

Na podstawie tego i odpowiedzi fwc: s utworzyłem pakiet instalowalny pip https://github.com/mkorpela/overrides

Od czasu do czasu kończę tutaj, patrząc na to pytanie. Zwykle dzieje się tak po (ponownym) zobaczeniu tego samego błędu w naszej bazie kodu: Ktoś zapomniał jakiegoś "interfejsu" implementującego klasę podczas zmiany nazwy metody w "interface" ..

Cóż, Python nie jest Javą, ale Python ma moc - i wyraźne jest lepsze niż ukryte - aw prawdziwym świecie są prawdziwe konkretne przypadki, w których to by mi pomogło.

Oto szkic dekoratora nadpisań. Spowoduje to sprawdzenie, czy klasa podana jako parametr ma taką samą nazwę metody (lub coś), co dekorowana metoda.

Jeśli możesz wymyślić lepsze rozwiązanie, opublikuj je tutaj!

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

Działa w następujący sposób:

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


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

a jeśli zrobisz błędną wersję, spowoduje to błąd asercji podczas ładowania klasy:

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

>> AssertionError!!!!!!!
mkorpela
źródło
17
Niesamowite. To złapało błąd pisowni, gdy pierwszy raz go wypróbowałem. Sława.
Christopher Bruns
7
mfbutner: nie jest wywoływana za każdym razem, gdy metoda jest wykonywana - tylko podczas tworzenia metody.
mkorpela
3
Jest to również przyjemne dla stringów dokumentacyjnych! overridesmoże skopiować łańcuch dokumentacyjny przesłoniętej metody, jeśli przesłaniająca metoda nie ma własnej.
letmaik
5
@mkorpela, heh ten twój kod powinien znajdować się w domyślnym systemie lib Pythona. Dlaczego nie umieścisz tego w systemie pip? : P
5
@mkorpela: Aha i proponuję poinformować programistów Pythona core o tym pakiecie, mogą rozważyć dodanie obout dekoratora do podstawowego systemu Pythona. :)
30

Oto implementacja, która nie wymaga podania nazwy 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
źródło
2
Trochę magiczne, ale znacznie ułatwia typowe użytkowanie. Czy możesz dołączyć przykłady użycia?
Bluu
jakie są średnie i najgorsze koszty użycia tego dekoratora, być może wyrażone jako porównanie z wbudowanym dekoratorem, takim jak @classmethod lub @property?
larham1
4
@ larham1 Ten dekorator jest wykonywany raz, podczas analizy definicji klasy, a nie przy każdym wywołaniu. Dlatego jego koszt wykonania jest nieistotny w porównaniu do czasu wykonania programu.
Abgan,
Będzie to dużo przyjemniejsze w Pythonie 3.6 dzięki PEP 487 .
Neil G,
Aby uzyskać lepszy komunikat o błędzie: assert any (hasattr (cls, method .__ name__) for cls in base_classes), „Overriden method" {} "not found in the base class.”. Format (method .__ name__)
Ivan Kovtun
14

Jeśli chcesz to tylko do celów dokumentacyjnych, możesz zdefiniować własny dekorator nadpisania:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

To naprawdę nic innego jak przyjemne dla oka, chyba że utworzysz override (f) w taki sposób, że faktycznie sprawdza, czy nadpisanie.

Ale w takim razie to jest Python, po co pisać tak, jakby to była Java?

Ber
źródło
2
Można dodać faktyczną walidację poprzez inspekcję do overridedekoratora.
Erik Kaplun
70
Ale w takim razie to jest Python, po co pisać tak, jakby była Java? Ponieważ niektóre pomysły w Javie są dobre i warte rozszerzenia na inne języki?
Piotr Dobrogost
9
Ponieważ kiedy zmieniasz nazwę metody w nadklasie, dobrze byłoby wiedzieć, że niektóre podklasy niższe o 2 poziomy ją nadpisują. Jasne, łatwo to sprawdzić, ale niewielka pomoc ze strony parsera języka nie zaszkodzi.
Abgan
4
Bo to dobry pomysł. Fakt, że wiele innych języków ma tę cechę, nie jest argumentem - ani za, ani przeciw.
sfkleach
6

Python to nie Java. Oczywiście nie ma czegoś takiego jak sprawdzanie w czasie kompilacji.

Myślę, że komentarz w dokumentacji jest mnóstwo. Dzięki temu każdy użytkownik Twojej metody może wpisać help(obj.method)i zobaczyć, że metoda jest zastąpieniem.

Możesz również jawnie rozszerzyć interfejs za pomocą class Foo(Interface), co pozwoli użytkownikom na wpisywanie w help(Interface.method)celu zorientowania się, jakie funkcje ma zapewnić Twoja metoda.

Tryptyk
źródło
57
Prawdziwym celem @Overridew Javie nie jest dokumentowanie - chodzi o wyłapanie błędu, gdy zamierzałeś nadpisać metodę, ale w końcu zdefiniowałeś nową (np. Ponieważ błędnie wpisałeś nazwę; w Javie może się to również zdarzyć, ponieważ użyłeś niewłaściwy podpis, ale nie jest to problem w Pythonie - ale błąd pisowni nadal istnieje).
Pavel Minaev
2
@ Pavel Minaev: To prawda, ale nadal wygodnie jest mieć dokumentację, zwłaszcza jeśli używasz edytora IDE / tekstu, który nie ma automatycznych wskaźników przesłonięć (na przykład JDT Eclipse pokazuje je starannie obok numerów wierszy).
Tuukka Mustonen
2
@PavelMinaev Wrong. Jednym z głównych punktów, @Overrideoprócz sprawdzania czasu kompilacji, jest dokumentacja.
siamii
6
@siamii Myślę, że pomoc w dokumentacji jest świetna, ale w całej oficjalnej dokumentacji Java, którą widzę, wskazują one tylko na znaczenie sprawdzania czasu kompilacji. Proszę uzasadnić swoje twierdzenie, że Paweł się „myli”.
Andrew Mellinger
5

Improwizując na @mkorpela świetna odpowiedź , oto wersja z

bardziej precyzyjne kontrole, nazewnictwo i podniesione obiekty Error

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


Oto jak to wygląda w praktyce:

NotImplementedErrornie zaimplementowano w klasie bazowej

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

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

skutkuje bardziej opisowym NotImplementedErrorbłędem

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

pełny stos

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


NotImplementedErroroczekiwany zaimplementowany typ

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

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

skutkuje bardziej opisowym NotImplementedErrorbłędem

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

pełny stos

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




Wspaniałą rzeczą w odpowiedzi @mkorpela jest to, że sprawdzanie odbywa się podczas fazy inicjalizacji. Kontrola nie musi być przeprowadzana. Odnosząc się do poprzednich przykładów, class Bnigdy nie jest inicjowany ( B()), ale NotImplementedErrornadal będzie podnosić. Oznacza to, że overridesbłędy są wykrywane wcześniej.

JamesThomasMoon1979
źródło
Hej! To wygląda interesująco. Czy możesz rozważyć wykonanie żądania ściągnięcia dla mojego projektu ipromise? Dodałem odpowiedź.
Neil G
@NeilG Rozwidliłem projekt ipromise i trochę zakodowałem. Wygląda na to, że zasadniczo zaimplementowałeś to wewnątrz overrides.py. Nie jestem pewien, co jeszcze mogę znacznie poprawić, poza zmianą typów wyjątków z TypeErrorna NotImplementedError.
JamesThomasMoon1979
Hej! Dzięki, nie mam sprawdzeń, czy zastąpiony obiekt faktycznie ma typ types.MethodType. To był dobry pomysł w twojej odpowiedzi.
Neil G
2

Jak powiedzieli inni, w przeciwieństwie do Javy, nie ma tagu @Overide, jednak powyżej możesz stworzyć własny za pomocą dekoratorów, jednak sugerowałbym użycie metody globalnej getattrib () zamiast używania wewnętrznego dyktowania, aby uzyskać coś takiego:

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

Jeśli chcesz, możesz złapać getattr () we własnej próbie catch, aby podnieść swój własny błąd, ale myślę, że metoda getattr jest lepsza w tym przypadku.

Również to przechwytuje wszystkie elementy związane z klasą, w tym metody klasowe i zmienne

Kłótnia x 2
źródło
2

Na podstawie @ wielką odpowiedź mkorpela za, pisałem podobny pakiet ( ipromise PyPI github ), który robi dużo więcej kontroli:

Załóżmy Adziedziczy od Bi C, Bdziedziczy od C.

Moduł ipromise sprawdza, czy:

  • Jeśli A.fnadpisuje B.f, B.fmusi istnieć i Amusi dziedziczyć z B. (To jest czek z pakietu overrides).

  • Nie masz wzorca, A.fktóry deklaruje, że zastępuje B.f, a następnie deklaruje, że zastępuje C.f. APowinien powiedzieć, że zastępuje from, C.fponieważ Bmoże zdecydować o zaprzestaniu zastępowania tej metody, a to nie powinno skutkować aktualizacjami podrzędnymi.

  • Nie masz wzorca, A.fktóry deklaruje przesłonięcie C.f, ale B.fnie deklaruje jego przesłonięcia.

  • Nie masz wzorca, A.fktóry deklaruje, że zastępuje C.f, ale B.fdeklaruje, że zastępuje niektóre D.f.

Posiada również różne funkcje do oznaczania i sprawdzania implementacji metody abstrakcyjnej.

Neil G.
źródło
0

Hear jest najprostszy i działa pod Jythonem z klasami 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()
user3034016
źródło
0

Dekorator, który zrobiłem, nie tylko sprawdził, czy nazwa nadpisującego atrybutu w klasie jest nadrzędną klasy, w której znajduje się atrybut, bez konieczności określania nadrzędnej klasy, ten dekorator również sprawdził, czy nadpisywany atrybut musi być tego samego typu, co nadpisany atrybut. Metody klas są traktowane jak metody, a metody statyczne są traktowane jak funkcje. Ten dekorator działa dla wywołań, metod klas, metod statycznych i właściwości.

Kod źródłowy można znaleźć pod adresem : https://github.com/fireuser909/override

Ten dekorator działa tylko w przypadku klas, które są instancjami override. Do testów uruchom moduł override .__ init__.

Michael
źródło
0

W Pythonie 2.6+ i Pythonie 3.2+ możesz to zrobić ( właściwie to symuluj , Python nie obsługuje przeciążania funkcji, a klasa potomna automatycznie zastępuje metodę rodzica). Możemy do tego użyć dekoratorów. Ale najpierw zwróć uwagę, że Python @decoratorsi Java @Annotationsto zupełnie różne rzeczy. Poprzednia jest opakowaniem z konkretnym kodem, a późniejsza jest flagą kompilatora.

W tym celu najpierw zrób 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)

wynik:

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

Zauważ, że musisz tutaj użyć dekoratora z nawiasami

Jedną rzeczą do zapamiętania jest to, że ponieważ Python nie ma bezpośredniego przeciążania funkcji, więc nawet jeśli klasa B nie dziedziczy z klasy A, ale potrzebuje wszystkich tych elementów, footo również musisz użyć @Override (chociaż użycie aliasu „Przeciążenie” będzie wyglądać lepiej w takim przypadku)

mradul
źródło