Коли використовувати 'raise NotImplementedError'?


103

Це нагадувати собі та своїй команді правильно виконувати клас? Я не в повній мірі використовую такий абстрактний клас:

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

    def cleanTileAtPosition(self, pos):
        raise NotImplementedError

    def isTileCleaned(self, m, n):
        raise NotImplementedError


2
Я б сказав: Коли це задовольняє "Принцип найменшого подиву" .
MSeifert

3
Це корисне запитання, про що свідчить чітка відповідь Урієля згідно з документами та відповідь на додану вартість щодо абстрактних базових класів від Жерома.
Давос,

Він також може бути використаний для позначення того, що похідний клас навмисно не реалізує абстрактний метод базового класу, який може запропонувати двосторонній захист. Дивіться нижче .
pfabri

Відповіді:


78

Як зазначено в документації [docs] ,

У визначених користувачем базових класах абстрактні методи повинні викликати цей виняток, коли вони вимагають, щоб похідні класи замінювали метод, або поки клас розробляється, щоб вказати, що реальну реалізацію все ще потрібно додати.

Зауважте, що, хоча основним заявленим випадком використання ця помилка є вказівкою на абстрактні методи, які слід реалізовувати у успадкованих класах, ви можете використовувати її як завгодно, наприклад, для позначення TODOмаркера.


6
Для абстрактних методів я волію використовувати abc(див. Мою відповідь ).
Jérôme

@ Джером, чому б не використовувати обидва? Прикрасьте abstractmethodі дайте піднятися NotImplementedError. Це забороняє super().method()реалізації methodв похідних класах.
timgeb

@timgeg Я не відчуваю потреби забороняти super (). method ().
Жером

50

Як каже Уріель , він призначений для методу в абстрактному класі, який повинен бути реалізований у дочірньому класі, але також може бути використаний для позначення TODO.

Існує альтернатива для першого випадку використання: абстрактні базові класи . Вони допомагають створювати абстрактні класи.

Ось приклад Python 3:

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

Під час створення екземпляра Cви отримаєте помилку, оскільки my_abstract_methodє абстрактною. Вам потрібно реалізувати це в дитячому класі.

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

Підклас Cта реалізація my_abstract_method.

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

Тепер ви можете створити екземпляр D.

C.my_abstract_methodне повинен бути порожнім. Це можна викликати з Dвикористання super().

Перевагою цього NotImplementedErrorє те, що ви отримуєте явний текст Exceptionпід час встановлення, а не під час виклику методу.


2
Доступно і в Python 2.6+. Просто from abc import ABCMeta, abstractmethodі визначте свій ABC за допомогою __metaclass__ = ABCMeta. Документи: docs.python.org/2/library/abc.html
BoltzmannBrain

Якщо ви хочете використовувати це з класом, який визначає метаклас, вам потрібно створити новий метаклас, який успадковує як початковий метаклас класу, так і ABCMeta.
rabbit.aaron

26

Поміркуйте, чи замість цього було:

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

    def cleanTileAtPosition(self, pos):
        pass

    def isTileCleaned(self, m, n):
        pass

і ви підклас і забули сказати йому, як, isTileCleaned()або, можливо, більш імовірно, друкувати його як isTileCLeaned(). Тоді у вашому коді ви отримаєте, Noneколи зателефонуєте йому.

  • Ви отримаєте замінену функцію, яку хотіли? Точно ні.
  • Чи є Noneдійсним результатом? Хто знає.
  • Це передбачувана поведінка? Майже напевно ні.
  • Ви отримаєте помилку? Це залежить.

raise NotImplmentedError змушує вас його реалізувати, оскільки це викличе виняток, коли ви намагаєтесь запустити його, поки цього не зробите. Це усуває багато тихих помилок. Це схоже на те, чому голий крім майже ніколи не є хорошою ідеєю : оскільки люди роблять помилки, і це гарантує, що їх не заносять під килимок.

Примітка: Використовувати абстрактний базовий клас, як зазначалося в інших відповідях, все-таки краще, оскільки тоді помилки попередньо завантажуються, і програма не запускатиметься, поки ви їх не реалізуєте (з NotImplementedError вона викличе виключення, лише якщо насправді викликано).


11

Можна також зробити raise NotImplementedError() внутрішній дочірній метод методу- @abstractmethodдекорованого базового класу.


Уявіть, що ви пишете сценарій управління для сімейства вимірювальних модулів (фізичних пристроїв). Функціональність кожного модуля є вузько визначеною, реалізуючи лише одну спеціальну функцію: одна може бути масивом реле, інша - багатоканальним ЦАП або АЦП, інша - амперметром тощо.

Значна частина команд низького рівня, що використовуються, буде спільно використовуватися між модулями, наприклад, для зчитування їх ідентифікаційних номерів або надсилання їм команди. Давайте подивимось, що ми маємо на даний момент:

Базовий клас

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

Спільні дієслова

Потім ми усвідомлюємо, що велика частина модульних командних дієслів і, отже, логіка їх інтерфейсів також є спільною. Ось 3 різні дієслова, значення яких є зрозумілим, враховуючи низку цільових модулів.

  • get(channel)

  • реле: увімкнути / вимкнути статус релеchannel

  • ЦАП: увімкніть вихідну напругуchannel

  • АЦП: увімкніть вхідну напругуchannel

  • enable(channel)

  • реле: увімкнути використання релеchannel

  • ЦАП: увімкнути використання вихідного каналуchannel

  • АЦП: увімкнути використання вхідного каналуchannel

  • set(channel)

  • реле:channel увімкнення / вимкнення реле

  • ЦАП: увімкнути вихідну напругуchannel

  • АЦП: хм ... нічого логічного не спадає на думку.


Спільні дієслова стають вимушеними дієсловами

Я міг би стверджувати, що існує сильний випадок, коли вищезазначені дієслова можна поділити між модулями, оскільки ми побачили, що їх значення очевидне для кожного з них. Я продовжував би писати свій базовий клас Genericтак:

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

    @abstractmethod
    def enable(self, channel):
        pass

    @abstractmethod
    def set(self, channel):
        pass

Підкласи

Зараз ми знаємо, що всі наші підкласи повинні будуть визначати ці методи. Давайте подивимося, як це може виглядати для модуля 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'

Вам зараз може бути цікаво:

Але це не буде працювати для модуля ADC, оскільки setтам немає сенсу, як ми щойно бачили це вище!

Ви маєте рацію: не реалізація set- це не варіант, оскільки Python тоді запускає помилку нижче, коли ви намагалися створити екземпляр об’єкта ADC.

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

Таким чином , ви повинні реалізувати що - то, тому що ми зробили виконання дієслова ( так званим «@abstractmethod»), який є загальним для двох інших модулів , але в той же час, ви також не повинні нічого реалізувати , як не має сенсу для даного конкретного модуля.setset

NotImplementedError to the Rescue

Заповнивши клас ADC таким чином:

class ADC(Generic): # ...continued

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

Ви робите три дуже гарні речі одночасно:

  1. Ви захищаєте користувача від помилкової команди ('set'), яка не (і не повинна!) Бути реалізованою для цього модуля.
  2. Ви прямо говорите їм, у чому проблема (див. Посилання TemporalWolf про "Голі винятки", чому це важливо)
  3. Ви захищаєте реалізацію всіх інших модулів, для яких вимушені дієслова мають сенс. Тобто ви гарантуєте, що ті модулі, для яких ці дієслова мають сенс, будуть реалізовувати ці методи, і що вони будуть робити це, використовуючи саме ці дієслова, а не деякі спеціальні імена.

-1

Ви можете використати @propertyдекоратор,

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

13
Я не розумію, як це вирішує питання, а коли це використовувати .
TemporalWolf
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.