Kiedy używać opcji „raise NotImplementedError”?

109

Czy ma to przypominać sobie i swojemu zespołowi o prawidłowym wdrożeniu zajęć? Nie w pełni korzystam z takiej klasy abstrakcyjnej:

class RectangularRoom(object):
    def __init__(self, width, height):
        raise NotImplementedError

    def cleanTileAtPosition(self, pos):
        raise NotImplementedError

    def isTileCleaned(self, m, n):
        raise NotImplementedError
Antonio Araujo
źródło
5
Duplikat między witrynami
2
Powiedziałbym: kiedy spełnia „zasadę najmniejszego zdziwienia” .
MSeifert
3
Jest to przydatne pytanie, o czym świadczy jasna odpowiedź Uriela zgodnie z dokumentacją oraz wartość dodana dotycząca abstrakcyjnych klas bazowych autorstwa Jérôme.
Davos
Można go również użyć do wskazania, że ​​klasa pochodna celowo nie implementuje metody abstrakcyjnej klasy bazowej, która może zapewnić dwukierunkową ochronę. Zobacz poniżej .
pfabri

Odpowiedzi:

81

Zgodnie z dokumentacją [docs] ,

W klasach bazowych zdefiniowanych przez użytkownika metody abstrakcyjne powinny zgłaszać ten wyjątek, gdy wymagają klas pochodnych, aby zastąpić metodę, lub gdy klasa jest opracowywana, aby wskazać, że rzeczywista implementacja nadal wymaga dodania.

Zauważ, że chociaż głównym podanym przypadkiem użycia ten błąd wskazuje na abstrakcyjne metody, które powinny być zaimplementowane w klasach dziedziczonych, możesz go użyć w dowolny sposób, na przykład do wskazania TODOznacznika.

Uriel
źródło
6
W przypadku metod abstrakcyjnych wolę używać abc(zobacz moją odpowiedź ).
Jérôme
@ Jérôme, dlaczego nie użyć obu? Udekoruj abstractmethodi pozwól mu się podnieść NotImplementedError. To zabrania super().method()w implementacjach methodklas pochodnych.
timgeb
@timgeg Nie czuję potrzeby zakazywania super (). method ().
Jérôme
52

Jak mówi Uriel , jest on przeznaczony dla metody w klasie abstrakcyjnej, która powinna być zaimplementowana w klasie potomnej, ale może być również używana do wskazywania TODO.

Istnieje alternatywa dla pierwszego przypadku użycia: abstrakcyjne klasy bazowe . Pomagają w tworzeniu klas abstrakcyjnych.

Oto przykład Pythona 3:

class C(abc.ABC):
    @abc.abstractmethod
    def my_abstract_method(self, ...):
        ...

Podczas tworzenia wystąpienia Cwystąpi błąd, ponieważ my_abstract_methodjest abstrakcyjny. Musisz zaimplementować to w klasie podrzędnej.

TypeError: Can't instantiate abstract class C with abstract methods my_abstract_method

Podklasa Ci implementacja my_abstract_method.

class D(C):
    def my_abstract_method(self, ...):
        ...

Teraz możesz utworzyć wystąpienie D.

C.my_abstract_methodnie musi być pusty. Można go wywołać Dza pomocą super().

Zaletą tego rozwiązania NotImplementedErrorjest to, że otrzymujesz jawną informację Exceptionw czasie tworzenia instancji, a nie w czasie wywołania metody.

Jérôme
źródło
2
Dostępne również w Pythonie 2.6+. Po prostu from abc import ABCMeta, abstractmethodzdefiniuj swoje ABC za pomocą __metaclass__ = ABCMeta. Dokumenty: docs.python.org/2/library/abc.html
BoltzmannBrain
Jeśli chcesz użyć tego z klasą, która definiuje metaklasę, musisz utworzyć nową metaklasę, która dziedziczy zarówno oryginalną metaklasę klasy, jak i ABCMeta.
rabbit.aaron
27

Zastanów się, czy zamiast tego było:

class RectangularRoom(object):
    def __init__(self, width, height):
        pass

    def cleanTileAtPosition(self, pos):
        pass

    def isTileCleaned(self, m, n):
        pass

a ty podklasujesz i zapominasz powiedzieć, jak to zrobić isTileCleaned()lub, co może bardziej prawdopodobne, wpisać to jako isTileCLeaned(). Następnie w swoim kodzie otrzymasz, Nonegdy go wywołasz.

  • Czy uzyskasz zastąpioną funkcję, którą chciałeś? Absolutnie nie.
  • Czy Nonedane wyjściowe są prawidłowe? Kto wie.
  • Czy to zamierzone zachowanie? Prawie na pewno nie.
  • Czy pojawi się błąd? To zależy.

raise NotImplmentedError zmusza cię do zaimplementowania go, ponieważ zgłosi wyjątek, gdy spróbujesz go uruchomić, dopóki tego nie zrobisz. To usuwa wiele cichych błędów. Jest to podobne do tego, dlaczego nagie z wyjątkiem prawie nigdy nie jest dobrym pomysłem : ponieważ ludzie popełniają błędy, a to zapewnia, że ​​nie zostaną zamiatani pod dywan.

Uwaga: użycie abstrakcyjnej klasy bazowej, jak wspomniały inne odpowiedzi, jest jeszcze lepsze, ponieważ wtedy błędy są ładowane z przodu i program nie będzie działał, dopóki ich nie zaimplementujesz (z NotImplementedError, wyrzuci wyjątek tylko wtedy, gdy zostanie faktycznie wywołany).

TemporalWolf
źródło
11

Można również wykonać raise NotImplementedError() wewnętrzną metodę @abstractmethod-dekorowaną metodę klasy bazowej.


Wyobraź sobie, że piszesz skrypt sterujący dla rodziny modułów pomiarowych (urządzeń fizycznych). Funkcjonalność każdego modułu jest wąsko zdefiniowana, realizując tylko jedną dedykowaną funkcję: jednym może być szereg przekaźników, innym wielokanałowym przetwornikiem cyfrowo-analogowym lub przetwornikiem cyfrowo-analogowym, innym amperomierzowi itp.

Wiele z używanych poleceń niskiego poziomu byłoby współdzielonych między modułami, na przykład w celu odczytania ich numerów identyfikacyjnych lub wysłania do nich polecenia. Zobaczmy, co mamy w tym momencie:

Klasa podstawowa

from abc import ABC, abstractmethod  #< we'll make use of these later

class Generic(ABC):
    ''' Base class for all measurement modules. '''

    # Shared functions
    def __init__(self):
        # do what you must...

    def _read_ID(self):
        # same for all the modules

    def _send_command(self, value):
        # same for all the modules

Czasowniki wspólne

Następnie zdajemy sobie sprawę, że wiele czasowników poleceń specyficznych dla modułu, a zatem logika ich interfejsów jest również współdzielona. Oto 3 różne czasowniki, których znaczenie byłoby oczywiste, biorąc pod uwagę liczbę modułów docelowych.

  • get(channel)

  • przekaźnik: uzyskaj stan włączenia / wyłączenia przekaźnikachannel

  • DAC: uzyskać wyjściowego napięcia nachannel

  • ADC: uzyskać wejściowego napięcia nachannel

  • enable(channel)

  • przekaźnik: włącz użycie przekaźnikachannel

  • DAC: włącz użycie kanału wyjściowegochannel

  • ADC: włącz używanie kanału wejściowegochannel

  • set(channel)

  • przekaźnik:channel włączanie / wyłączanie przekaźnika

  • DAC: ustawienie wyjściowe napięcie nachannel

  • ADC: hmm ... nic logicznego nie przychodzi na myśl.


Czasowniki współdzielone stają się czasownikami wymuszonymi

Twierdzę, że powyższe czasowniki powinny być współużytkowane przez moduły, ponieważ widzieliśmy, że ich znaczenie jest oczywiste dla każdego z nich. Kontynuowałbym pisanie mojej klasy bazowej w ten Genericsposób:

class Generic(ABC):  # ...continued
    
    @abstractmethod
    def get(self, channel):
        pass

    @abstractmethod
    def enable(self, channel):
        pass

    @abstractmethod
    def set(self, channel):
        pass

Podklasy

Teraz wiemy, że wszystkie nasze podklasy będą musiały zdefiniować te metody. Zobaczmy, jak mógłby wyglądać moduł ADC:

class ADC(Generic):

    def __init__(self):
        super().__init__()  #< applies to all modules
        # more init code specific to the ADC module
    
    def get(self, channel):
        # returns the input voltage measured on the given 'channel'

    def enable(self, channel):
        # enables accessing the given 'channel'

Możesz się teraz zastanawiać:

Ale to nie zadziała dla modułu ADC , ponieważ setnie ma to sensu, ponieważ widzieliśmy to powyżej!

Masz rację: brak implementacji setnie jest opcją, ponieważ Python wywołałby poniższy błąd podczas próby utworzenia instancji obiektu ADC.

TypeError: Can't instantiate abstract class 'ADC' with abstract methods 'set'

Więc trzeba zaimplementować coś, bo zrobiliśmy egzekwowane czasownik (aka „@abstractmethod”), który jest wspólny dla dwóch innych modułów, ale w tym samym czasie, należy również wdrożyć jako nic nie nie ma sensu dla tego konkretnego modułu.setset

NotImplementedError to the Rescue

Ukończenie klasy ADC w ten sposób:

class ADC(Generic): # ...continued

    def set(self, channel):
        raise NotImplementedError("Can't use 'set' on an ADC!")

Robisz jednocześnie trzy bardzo dobre rzeczy:

  1. Chronisz użytkownika przed błędnym wydaniem polecenia („set”), które nie jest (i nie powinno!) Być zaimplementowane dla tego modułu.
  2. Mówisz im wyraźnie, na czym polega problem (patrz link TemporalWolf o `` Nagich wyjątkach '', aby dowiedzieć się, dlaczego jest to ważne)
  3. Chronisz implementację wszystkich innych modułów, dla których egzekwowane czasowniki mają sens. Oznacza to, że upewniasz się, że te moduły, dla których te czasowniki mają sens, zaimplementują te metody i że będą to robić przy użyciu dokładnie tych czasowników, a nie innych nazw ad-hoc.
pfabri
źródło
-1

Możesz użyć @propertydekoratora,

>>> class Foo():
...     @property
...     def todo(self):
...             raise NotImplementedError("To be implemented")
... 
>>> f = Foo()
>>> f.todo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in todo
NotImplementedError: To be implemented
Simeon Aleksov
źródło
13
Nie rozumiem, jak to odpowiada na pytanie, kiedy go użyć .
TemporalWolf