¿Cuándo usar 'raise NotImplementedError'?

102

¿Es para recordarse a sí mismo y a su equipo que deben implementar la clase correctamente? No consigo el uso completo de una clase abstracta como esta:

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
fuente
5
Incautación
2
Yo diría: Cuando satisfaga el "Principio del menor asombro" .
MSeifert
3
Esta es una pregunta útil, como lo demuestra la respuesta clara de Uriel según los documentos, y la respuesta de valor agregado con respecto a las clases base abstractas de Jérôme.
Davos
También se puede utilizar para indicar que una clase derivada no implementa intencionalmente un método abstracto de clase base, que puede ofrecer una protección bidireccional. Vea abajo .
pfabri

Respuestas:

78

Como dice la documentación [docs] ,

En las clases base definidas por el usuario, los métodos abstractos deben generar esta excepción cuando requieren clases derivadas para anular el método, o mientras la clase se está desarrollando para indicar que la implementación real aún debe agregarse.

Tenga en cuenta que, aunque el caso de uso principal declarado, este error es la indicación de métodos abstractos que deben implementarse en clases heredadas, puede usarlo de la forma que desee, como para indicar un TODOmarcador.

Uriel
fuente
6
Para métodos abstractos, prefiero usar abc(ver mi respuesta ).
Jérôme
@ Jérôme, ¿por qué no usar ambos? Decora con abstractmethody deja que suba NotImplementedError. Esto prohíbe super().method()en implementaciones de methodclases derivadas.
timgeb
@timgeg No siento la necesidad de prohibir super (). method ().
Jérôme
49

Como dice Uriel , está destinado a un método en una clase abstracta que debe implementarse en la clase secundaria, pero que también se puede usar para indicar un TODO.

Existe una alternativa para el primer caso de uso: clases base abstractas . Aquellos ayudan a crear clases abstractas.

Aquí hay un ejemplo de Python 3:

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

Al crear una instancia C, obtendrá un error porque my_abstract_methodes abstracto. Necesitas implementarlo en una clase secundaria.

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

Subclase Ce implemento my_abstract_method.

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

Ahora puedes crear una instancia D.

C.my_abstract_methodno tiene que estar vacío. Se puede llamar Dusando super().

Una ventaja de esto NotImplementedErrores que obtiene un explícito Exceptionen el momento de la instanciación, no en el momento de la llamada al método.

Jérôme
fuente
2
Disponible también en Python 2.6+. Solo from abc import ABCMeta, abstractmethody define tu ABC con __metaclass__ = ABCMeta. Documentos: docs.python.org/2/library/abc.html
BoltzmannBrain
Si desea usar esto con una clase que define una metaclase, necesita crear una nueva metaclase que herede tanto la metaclase original de la clase como ABCMeta.
rabbit.aaron
26

Considere si en cambio fue:

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

    def cleanTileAtPosition(self, pos):
        pass

    def isTileCleaned(self, m, n):
        pass

y usted subclasifica y se olvida de decirle cómo isTileCleaned()o, quizás más probablemente, escribe como isTileCLeaned(). Luego, en su código, obtendrá un Nonecuando lo llame.

  • ¿Obtendrá la función anulada que deseaba? Definitivamente no.
  • ¿Es Noneuna salida válida? Quién sabe.
  • ¿Es ese comportamiento previsto? Es casi seguro que no.
  • ¿Recibirá un error? Depende.

raise NotImplmentedError fuerzas que ponerlo en práctica, ya que será una excepción cuando intenta ejecutar hasta que lo haga. Esto elimina muchos errores silenciosos. Es similar a la razón por la que una excepción desnuda casi nunca es una buena idea : porque la gente comete errores y esto asegura que no se barren debajo de la alfombra.

Nota: Usar una clase base abstracta, como han mencionado otras respuestas, es mejor aún, ya que entonces los errores se cargan por adelantado y el programa no se ejecutará hasta que los implemente (con NotImplementedError, solo lanzará una excepción si realmente se llama).

TemporalWolf
fuente
11

También se podría hacer un método raise NotImplementedError() dentro del hijo de un método de @abstractmethodclase base decorado.


Imagine escribir un script de control para una familia de módulos de medición (dispositivos físicos). La funcionalidad de cada módulo se define de manera estricta, implementando solo una función dedicada: una podría ser una matriz de relés, otra un DAC o ADC multicanal, otra un amperímetro, etc.

Gran parte de los comandos de bajo nivel en uso se compartirían entre los módulos, por ejemplo, para leer sus números de identificación o enviarles un comando. Veamos qué tenemos en este punto:

Clase base

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

Verbos compartidos

Entonces nos damos cuenta de que gran parte de los verbos de comando específicos del módulo y, por lo tanto, la lógica de sus interfaces también se comparte. Aquí hay 3 verbos diferentes cuyo significado se explica por sí mismo considerando una serie de módulos de destino.

  • get(channel)

  • relé: activa el estado de encendido / apagado del reléchannel

  • DAC: activa el voltaje de salidachannel

  • ADC: activa el voltaje de entradachannel

  • enable(channel)

  • relé: habilita el uso del relé enchannel

  • DAC: habilita el uso del canal de salida enchannel

  • ADC: habilita el uso del canal de entrada enchannel

  • set(channel)

  • relé: enciende channel/ apaga el relé

  • DAC: configure el voltaje de salida enchannel

  • ADC: hmm ... no se me ocurre nada lógico .


Los verbos compartidos se convierten en verbos obligatorios

Yo diría que hay un caso sólido para que los verbos anteriores se compartan entre los módulos, ya que vimos que su significado es evidente para cada uno de ellos. Continuaría escribiendo mi clase base Genericasí:

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

    @abstractmethod
    def enable(self, channel):
        pass

    @abstractmethod
    def set(self, channel):
        pass

Subclases

Ahora sabemos que todas nuestras subclases tendrán que definir estos métodos. Veamos cómo se vería para el módulo 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'

Puede que ahora te estés preguntando:

¡Pero esto no funcionará para el módulo ADC ya setque no tiene sentido allí ya que acabamos de ver esto arriba!

Tienes razón: no implementar no setes una opción, ya que Python dispararía el siguiente error cuando intentas crear una instancia de tu objeto ADC.

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

Entonces debes implementar algo, porque creamos setun verbo forzado (también conocido como '@abstractmethod'), que es compartido por otros dos módulos pero, al mismo tiempo, tampoco debes implementar nada, ya setque no tiene sentido para este módulo en particular.

NotImplementedError al rescate

Al completar la clase ADC de esta manera:

class ADC(Generic): # ...continued

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

Estás haciendo tres cosas muy buenas a la vez:

  1. Está protegiendo a un usuario de la emisión errónea de un comando ('set') que no está (¡y no debería!) Implementarse para este módulo.
  2. Les está diciendo explícitamente cuál es el problema (consulte el enlace de TemporalWolf sobre 'Excepciones desnudas' para saber por qué esto es importante)
  3. Está protegiendo la implementación de todos los demás módulos para los que los verbos forzados tienen sentido. Es decir, se asegura de que los módulos para los que estos verbos tienen sentido implementarán estos métodos y que lo harán utilizando exactamente estos verbos y no algunos otros nombres ad-hoc.
pfabri
fuente
-1

Es posible que desee utilizar el @propertydecorador,

>>> 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
fuente
13
No veo cómo esto aborda la cuestión de cuándo usarlo .
TemporalWolf