Maneira correta de declarar exceções personalizadas no Python moderno?


1291

Qual é a maneira correta de declarar classes de exceção personalizadas no Python moderno? Meu principal objetivo é seguir o padrão de outras classes de exceção, para que (por exemplo) qualquer sequência extra que eu inclua na exceção seja impressa por qualquer ferramenta capturada pela exceção.

Por "Python moderno", quero dizer algo que será executado no Python 2.5, mas será "correto" para a maneira de fazer as coisas do Python 2.6 e do Python 3. *. Por "personalizado", quero dizer um objeto Exception que pode incluir dados extras sobre a causa do erro: uma string, talvez também algum outro objeto arbitrário relevante para a exceção.

Fui enganado pelo seguinte aviso de reprovação no Python 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

Parece loucura que BaseExceptiontem um significado especial para os atributos nomeados message. Pelo PEP-352, entendo que esse atributo tinha um significado especial no 2.5 que eles estão tentando depreciar, então acho que esse nome (e somente esse) agora é proibido? Ugh.

Também estou ciente de que Exceptiontem algum parâmetro mágico args, mas nunca soube usá-lo. Nem tenho certeza de que é o caminho certo para fazer as coisas daqui para frente; muitas discussões que encontrei on-line sugeriam que eles estavam tentando acabar com os argumentos do Python 3.

Atualização: duas respostas sugeriram a substituição __init__e __str__/ __unicode__/ __repr__. Parece muita digitação, é necessário?

Respostas:


1323

Talvez eu tenha perdido a pergunta, mas por que não:

class MyException(Exception):
    pass

Edit: para substituir algo (ou passar argumentos extras), faça o seguinte:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

Dessa forma, você pode passar o dict de mensagens de erro para o segundo parâmetro e acessá-lo mais tarde com e.errors


Atualização do Python 3: No Python 3+, você pode usar este uso um pouco mais compacto de super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors

35
No entanto, uma exceção definida como essa não seria selecionável; veja a discussão aqui stackoverflow.com/questions/16244923/…
jiakai

87
@jiakai significa "selecionável". :-)
Robino 15/09

1
Após a documentação do python para exceções definidas pelo usuário, os nomes mencionados na função __init__ estão incorretos. Em vez de (eu, mensagem, erro) é (eu, expressão, mensagem). A expressão de atributo é a expressão de entrada na qual o erro ocorreu e a mensagem é uma explicação do erro.
ddleon 27/02

2
Isso é um mal-entendido, @ddleon. O exemplo nos documentos aos quais você está se referindo é para um caso de uso específico. Não há significado para o nome dos argumentos construtores da subclasse (nem seu número).
asthasr 22/04

498

Com exceções Python modernos, você não precisa de abuso .messageou substituição .__str__()ou .__repr__()ou nada disso. Se tudo o que você deseja é uma mensagem informativa quando sua exceção é gerada, faça o seguinte:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

Isso dará um retorno final com MyException: My hovercraft is full of eels.

Se você quiser mais flexibilidade com a exceção, pode passar um dicionário como argumento:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

No entanto, obter esses detalhes em um exceptbloco é um pouco mais complicado. Os detalhes são armazenados no argsatributo, que é uma lista. Você precisaria fazer algo assim:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

Ainda é possível passar vários itens para a exceção e acessá-los por meio de índices de tupla, mas isso é altamente desencorajado (e foi planejado para desaprovação há algum tempo). Se você precisar de mais do que uma única informação e o método acima não for suficiente para você, você deve subclassificar Exceptionconforme descrito no tutorial .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message

2
"mas isso será descontinuado no futuro" - isso ainda se destina à descontinuação? O Python 3.7 ainda parece aceitar com satisfação Exception(foo, bar, qux).
Mtraceur

Ele não viu nenhum trabalho recente para privá-lo desde que a última tentativa falhou devido à dor da transição, mas esse uso ainda é desencorajado. Vou atualizar minha resposta para refletir isso.
Frnknstn

@frnknstn, por que é desencorajado? Parece um bom idioma para mim.
Neves

2
Para começar, o uso de tuplas para armazenar informações de exceção não beneficia o uso de um dicionário para fazer o mesmo. Se você estiver interessado no raciocínio por trás das alterações de exceção, dê uma olhada no PEP352
frnknstn

A seção relevante do PEP352 é "Idéias Retraídas " .
liberforce

196

"Maneira correta de declarar exceções personalizadas no Python moderno?"

Isso é bom, a menos que sua exceção seja realmente um tipo de exceção mais específica:

class MyException(Exception):
    pass

Ou melhor (talvez perfeito), em vez de passdar uma sequência de caracteres:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Subclasses de exceção de subclasse

Dos documentos

Exception

Todas as exceções integradas que não saem do sistema são derivadas dessa classe. Todas as exceções definidas pelo usuário também devem ser derivadas dessa classe.

Isso significa que, se sua exceção for um tipo de exceção mais específica, subclasse essa exceção em vez da genérica Exception(e o resultado será o de que você ainda deriva, Exceptionconforme recomendado pela documentação). Além disso, você pode pelo menos fornecer uma string de documento (e não ser forçado a usar a passpalavra - chave):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Defina os atributos que você mesmo cria com um costume __init__. Evite passar um ditado como argumento posicional, os futuros usuários do seu código agradecerão. Se você usar o atributo de mensagem descontinuada, atribuir você mesmo evitará DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

Não há realmente nenhuma necessidade de escrever o seu próprio __str__ou __repr__. Os embutidos são muito legais e sua herança cooperativa garante que você o use.

Crítica da resposta principal

Talvez eu tenha perdido a pergunta, mas por que não:

class MyException(Exception):
    pass

Novamente, o problema com o exposto acima é que, para capturá-lo, você precisará nomeá-lo especificamente (importando-o se criado em outro lugar) ou capturar Exception (mas você provavelmente não está preparado para lidar com todos os tipos de exceções, e você só deve pegar as exceções que está preparado para lidar). Críticas semelhantes às abaixo, mas, além disso, não é assim que se inicializa via super, e você receberá uma DeprecationWarningse acessar o atributo de mensagem:

Edit: para substituir algo (ou passar argumentos extras), faça o seguinte:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

Dessa forma, você pode passar o dict de mensagens de erro para o segundo parâmetro e acessá-lo mais tarde com e.errors

Também requer exatamente dois argumentos a serem passados ​​(além do self.) Nem mais, nem menos. Essa é uma restrição interessante que os usuários futuros podem não apreciar.

Ser direto - viola a substituibilidade de Liskov .

Vou demonstrar os dois erros:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

Comparado com:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'

2
Olá a partir de 2018! BaseException.messagese foi no Python 3, então a crítica só vale para versões antigas, certo?
Kos

5
@ Kos A crítica sobre a substituibilidade de Liskov ainda é válida. A semântica do primeiro argumento como uma "mensagem" também é questionável, mas acho que não vou argumentar. Vou dar uma olhada mais quando tiver mais tempo livre.
Aaron Hall

1
FWIW, para Python 3 (pelo menos para 3.6 ou mais), seria possível redefinir o __str__método em MyAppValueErrorvez de confiar no messageatributo
Jacquot

1
@AaronHall Você poderia expandir o benefício da subclasse ValueError em vez de Exception? Você afirma que é isso que os documentos significam, mas uma leitura direta não suporta essa interpretação e, no Tutorial do Python, em Exceções definidas pelo usuário, torna-a claramente a escolha do usuário: "As exceções normalmente devem ser derivadas da classe Exception, direta ou indiretamente ". Por isso, gostaria de entender se sua opinião é justificável, por favor.
Ostergaard # 16/18

1
@ostergaard Não é possível responder por completo agora, mas, em suma, o usuário tem a opção adicional de capturar ValueError. Isso faz sentido se estiver na categoria de erros de valor. Se não estiver na categoria de erros de valor, eu argumentaria contra isso na semântica. Há espaço para algumas nuances e raciocínios por parte do programador, mas prefiro especificidade quando aplicável. Vou atualizar minha resposta para melhor abordar o assunto em breve.
Aaron Hall

50

veja como as exceções funcionam por padrão se um vs mais atributos forem usados ​​(rastreamentos omitidos):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

então você pode querer ter uma espécie de " modelo de exceção ", funcionando como uma exceção em si, de maneira compatível:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

isso pode ser feito facilmente com esta subclasse

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

e se você não gostar da representação padrão de tupla, adicione __str__ método à ExceptionTemplateclasse, como:

    # ...
    def __str__(self):
        return ': '.join(self.args)

e você terá

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken

32

A partir do Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), o método recomendado ainda é:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

Não se esqueça de documentar por que uma exceção personalizada é necessária!

Se você precisar, este é o caminho a seguir para exceções com mais dados:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

e buscá-los como:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=Noneé importante torná-lo em decapagem. Antes de descartá-lo, você deve ligar paraerror.__reduce__() . O carregamento funcionará conforme o esperado.

Talvez você deva investigar para encontrar uma solução usando a returninstrução pythons se precisar de muitos dados a serem transferidos para alguma estrutura externa. Isso parece ser mais claro / mais pitônico para mim. Exceções avançadas são muito usadas em Java, que às vezes pode ser irritante ao usar uma estrutura e ter que detectar todos os erros possíveis.


1
No mínimo, os documentos atuais indicam que essa é a maneira de fazer isso (pelo menos sem as __str__) e não outras respostas que usam super().__init__(...). Apenas uma pena que substitui __str__e__repr__ provavelmente é necessária apenas para uma melhor serialização "padrão".
kevlarr

2
Pergunta honesta: Por que é importante que as exceções sejam capazes de decapagem? Quais são os casos de uso para exceções de dumping e carregamento?
Roel Schroeven

1
@RoelSchroeven: tive que paralelizar o código uma vez. Executou um processo único, mas os aspectos de algumas de suas classes não eram serializáveis ​​(a função lambda era passada como objeto). Levei algum tempo para descobrir e consertar. Ou seja, alguém mais tarde pode acabar precisando serializar seu código, não conseguir fazê-lo e precisar descobrir por quê ... Meu problema não foi um erro imperceptível, mas posso vê-lo causando problemas semelhantes.
logicOnAbstractions

17

Você deve substituir __repr__ou__unicode__ métodos, em vez de usar a mensagem, os argumentos que você fornece ao construir a exceção estarão no argsatributo do objeto de exceção.


7

Não, "mensagem" não é proibida. É apenas obsoleto. Seu aplicativo funcionará bem com o uso da mensagem. Mas você pode se livrar do erro de descontinuação, é claro.

Quando você cria classes Exception personalizadas para seu aplicativo, muitas delas não são subclasses apenas de Exception, mas de outras, como ValueError ou similar. Então você tem que se adaptar ao uso de variáveis.

E se você tiver muitas exceções em seu aplicativo, geralmente é uma boa ideia ter uma classe base personalizada comum para todos eles, para que os usuários de seus módulos possam fazer

try:
    ...
except NelsonsExceptions:
    ...

E nesse caso, você pode fazer o __init__ and __str__ necessário lá, para não precisar repeti-lo para todas as exceções. Mas simplesmente chamar a variável de mensagem de algo mais do que a mensagem funciona.

De qualquer forma, você só precisa __init__ or __str__se fizer algo diferente do que a própria exceção faz. E, se a depreciação for necessária, você precisará das duas ou receberá um erro. Não é necessário muito código extra por classe. ;)


É interessante que as exceções do Django não herdam de uma base comum. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Você tem um bom exemplo ao capturar todas as exceções de um aplicativo específico? (talvez seja útil apenas para alguns tipos específicos de aplicativos).
Yaroslav Nikitenko

Encontrei um bom artigo sobre esse tópico, julien.danjou.info/python-exceptions-guide . Eu acho que as exceções devem ser subclassificadas principalmente com base no domínio, não com base no aplicativo. Quando seu aplicativo é sobre o protocolo HTTP, você deriva de HTTPError. Quando parte do seu aplicativo é TCP, você obtém as exceções dessa parte do TCPError. Mas se o seu aplicativo abranger muitos domínios (arquivo, permissões, etc.), o motivo para ter uma MyBaseException diminui. Ou é para proteger de 'violação de camada'?
Yaroslav Nikitenko

6

Veja um artigo muito bom " O guia definitivo para exceções do Python ". Os princípios básicos são:

  • Sempre herde de (pelo menos) Exceção.
  • Sempre chame BaseException.__init__com apenas um argumento.
  • Ao criar uma biblioteca, defina uma classe base herdada de Exception.
  • Forneça detalhes sobre o erro.
  • Herdar de tipos de exceções integradas quando faz sentido.

Há também informações sobre organização (em módulos) e exceções de empacotamento, recomendo a leitura do guia.


1
Este é um bom exemplo de por que, no SO, costumo verificar a resposta mais votada, mas também as mais recentes. Além útil, obrigado.
logicOnAbstractions

1
Always call BaseException.__init__ with only one argument.Parece uma restrição desnecessária, pois na verdade aceita qualquer número de argumentos.
Eugene Yarmash 29/02

@EugeneYarmash Eu concordo, agora não entendo isso. Eu não uso de qualquer maneira. Talvez devesse reler o artigo e expandir minha resposta.
Yaroslav Nikitenko

@EugeneYarmash Eu li o artigo novamente. É afirmado que, no caso de vários argumentos, a implementação C chama "return PyObject_Str (self-> args);" Isso significa que uma string deve funcionar melhor que várias. Você checou isso?
Yaroslav Nikitenko 03/03

3

Tente este exemplo

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")

1

Para definir corretamente suas próprias exceções, existem algumas práticas recomendadas que você precisa seguir:

  • Defina uma classe base herdada de Exception. Isso permitirá capturar quaisquer exceções relacionadas ao projeto (exceções mais específicas devem herdar dele):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Organizar essas classes de exceção em um módulo separado (por exemplo exceptions.py) geralmente é uma boa ideia.

  • Para criar uma exceção personalizada, subclasse a classe de exceção base.

  • Para adicionar suporte a argumentos extras a uma exceção personalizada, defina um __init__()método personalizado com um número variável de argumentos. Chame a classe base __init__(), passando quaisquer argumentos posicionais para ela (lembre-se de que BaseException/Exception espera qualquer número de argumentos posicionais ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Para gerar essa exceção com um argumento extra, você pode usar:

    raise CustomError('Something bad happened', foo='foo')

Esse design segue o princípio de substituição de Liskov , pois você pode substituir uma instância de uma classe de exceção base por uma instância de uma classe de exceção derivada. Além disso, permite criar uma instância de uma classe derivada com os mesmos parâmetros que o pai.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.