Когда использовать «поднять NotImplementedError»?

102

Чтобы напомнить себе и своей команде о правильной реализации класса? Я не могу полностью использовать такой абстрактный класс:

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

    def cleanTileAtPosition(self, pos):
        raise NotImplementedError

    def isTileCleaned(self, m, n):
        raise NotImplementedError
Антонио Араухо
источник
5
Межсайтовый обман: softwareengineering.stackexchange.com/q/231397/110531
jonrsharpe 01
2
Я бы сказал: когда это удовлетворяет «принципу наименьшего удивления» .
MSeifert 01
3
Это полезный вопрос, о чем свидетельствует четкий ответ Уриэля в соответствии с документацией и ответ Жерома относительно абстрактных базовых классов.
Давос
Его также можно использовать, чтобы указать, что производный класс намеренно не реализует абстрактный метод базового класса, который может обеспечивать двустороннюю защиту. См. Ниже .
пфабри

Ответы:

78

Как указано в документации [docs] ,

В определяемых пользователем базовых классах абстрактные методы должны вызывать это исключение, когда они требуют, чтобы производные классы переопределили метод, или пока класс разрабатывается, чтобы указать, что реальная реализация все еще нуждается в добавлении.

Обратите внимание, что хотя основным заявленным вариантом использования является указание на абстрактные методы, которые должны быть реализованы в унаследованных классах, вы можете использовать ее как угодно, например, для указания TODOмаркера.

Уриил
источник
6
Для абстрактных методов я предпочитаю использовать abc(см. Мой ответ ).
Жером
@ Jérôme, почему бы не использовать оба? Украсить abstractmethodи дать подняться NotImplementedError. Это запрещено super().method()в реализациях methodв производных классах.
timgeb
@timgeg Я не чувствую необходимости запрещать super (). method ().
Жером,
49

Как говорит Уриэль , он предназначен для метода в абстрактном классе, который должен быть реализован в дочернем классе, но может также использоваться для указания 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и определите свою азбуку с помощью __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 он будет генерировать исключение только в случае фактического вызова).

TemporalWolf
источник
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

  • ADC: разрешить использование входного канала наchannel

  • set(channel)

  • реле: включить channel/ выключить реле

  • ЦАП: установите выходное напряжение наchannel

  • ADC: хм ... ничего логичного в голову не приходит.


Общие глаголы становятся принудительными глаголами

Я бы сказал, что есть веские основания для использования вышеуказанных глаголов в модулях, поскольку мы видели, что их значение очевидно для каждого из них. Я бы продолжил писать свой базовый класс 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 спешит на помощь

Заполнив класс 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