Os blocos try / except aninhados em python são uma boa prática de programação?

201

Estou escrevendo meu próprio contêiner, que precisa dar acesso a um dicionário dentro por chamadas de atributo. O uso típico do contêiner seria assim:

dict_container = DictContainer()
dict_container['foo'] = bar
...
print dict_container.foo

Sei que pode ser estúpido escrever algo assim, mas essa é a funcionalidade que preciso fornecer. Eu estava pensando em implementar isso da seguinte maneira:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        try:
            return self.dict[item]
        except KeyError:
            print "The object doesn't have such attribute"

Não tenho certeza se os blocos try / except aninhados são uma boa prática; portanto, outra maneira seria usar hasattr()e has_key():

def __getattribute__(self, item):
        if hasattr(self, item):
            return object.__getattribute__(item)
        else:
            if self.dict.has_key(item):
                return self.dict[item]
            else:
                raise AttributeError("some customised error")

Ou, para usar um deles e tentar um bloco de captura como este:

def __getattribute__(self, item):
    if hasattr(self, item):
        return object.__getattribute__(item)
    else:
        try:
            return self.dict[item]
        except KeyError:
            raise AttributeError("some customised error")

Qual opção é mais pitônica e elegante?

Michal
fonte
Apreciaria os deuses python fornecendo if 'foo' in dict_container:. Amém.
gseattle

Respostas:

180

Seu primeiro exemplo está perfeitamente bem. Até os documentos oficiais do Python recomendam esse estilo conhecido como EAFP .

Pessoalmente, prefiro evitar o aninhamento quando não for necessário:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass  # fallback to dict
    try:
        return self.dict[item]
    except KeyError:
        raise AttributeError("The object doesn't have such attribute") from None

PS. has_key()foi descontinuado por muito tempo no Python 2. Use em item in self.dictvez disso.

lqc
fonte
2
return object.__getattribute__(item)está incorreto e produzirá um TypeErrorporque o número errado de argumentos está sendo passado. Em vez disso, deveria ser return object.__getattribute__(self, item).
martineau
13
PEP 20: flat é melhor que aninhado.
Ioannis Filippidis
7
O que from Nonesignifica na última linha?
Niklas 27/03
2
@niklas Essencialmente, suprime o contexto da exceção ("durante o tratamento dessa exceção ocorreu outra exceção" - mensagens esque). Veja aqui
Kade 27/02
O fato de os documentos do Python recomendarem aninhamento de try's é meio insano. É obviamente um estilo horrendo. A maneira correta de lidar com uma cadeia de operações onde as coisas podem falhar dessa maneira seria usar algum tipo de construção monádica, que o Python não suporta.
Henry Henrinson 18/09/19
19

Enquanto em Java é realmente uma má prática usar Exceções para controle de fluxo (principalmente porque as exceções forçam a jvm a reunir recursos ( mais aqui )), em Python você tem 2 princípios importantes: Duck Typing e EAFP . Isso basicamente significa que você é incentivado a tentar usar um objeto da maneira que você acha que ele funcionaria, e a lidar quando as coisas não são assim.

Em resumo, o único problema seria seu código ficar muito recuado. Se você quiser, tente simplificar alguns dos aninhamentos, como o lqc sugerido

Bruno Penteado
fonte
10

Para o seu exemplo específico, você realmente não precisa aninhá-los. Se a expressão no trybloco for bem-sucedida, a função retornará, portanto, qualquer código após todo o bloco try / except será executado apenas se a primeira tentativa falhar. Então você pode fazer:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass
    # execution only reaches here when try block raised AttributeError
    try:
        return self.dict[item]
    except KeyError:
        print "The object doesn't have such attribute"

Aninhando-os não é ruim, mas sinto que deixá-lo plano torna a estrutura mais clara: você está tentando seqüencialmente uma série de coisas e retornando a primeira que funciona.

Aliás, você pode querer pensar se realmente deseja usar em __getattribute__vez de __getattr__aqui. O uso __getattr__simplificará as coisas porque você saberá que o processo normal de pesquisa de atributos já falhou.

BrenBarn
fonte
10

Apenas tenha cuidado - neste caso, primeiro finallyé tocado, mas também ignorado.

def a(z):
    try:
        100/z
    except ZeroDivisionError:
        try:
            print('x')
        finally:
            return 42
    finally:
        return 1


In [1]: a(0)
x
Out[1]: 1
Sławomir Lenart
fonte
Uau, isso me impressiona ... Você poderia me indicar algum fragmento de documentação que explique esse comportamento?
Michal
1
@Michal: fyi: ambos os finallyblocos são executados a(0), mas apenas o pai finally-returné retornado.
Sławomir Lenart 28/03
7

Na minha opinião, essa seria a maneira mais pitônica de lidar com isso, embora e porque torne sua pergunta discutível. Observe que isso define em __getattr__()vez de __getattribute__()fazê-lo, porque isso significa que ele só precisa lidar com os atributos "especiais" mantidos no dicionário interno.

def __getattr__(self, name):
    """only called when an attribute lookup in the usual places has failed"""
    try:
        return self.my_dict[name]
    except KeyError:
        raise AttributeError("some customized error message")
Martineau
fonte
2
Observe que gerar uma exceção em um exceptbloco pode gerar resultados confusos no Python 3. Isso ocorre porque (de acordo com o PEP 3134) o Python 3 rastreia a primeira exceção (the KeyError) como o "contexto" da segunda exceção (the AttributeError) e se atinge o valor nível superior, ele imprimirá um rastreamento que inclui as duas exceções. Isso pode ser útil quando uma segunda exceção não era esperada, mas se você deliberadamente criar a segunda exceção, é indesejável. Para Python 3.3, o PEP 415 adicionou a capacidade de suprimir o contexto usando raise AttributeError("whatever") from None.
Blckknght
3
@ Blckknght: Imprimir um traceback que inclua as duas exceções seria bom neste caso. Em outras palavras, não acho que sua afirmação geral de que sempre é indesejável seja verdadeira. No uso aqui, transformar um KeyErrorem AttributeErrore mostrar que foi o que aconteceu em um retorno seria útil e apropriado.
martineau
Para situações mais complicadas, você pode estar certo, mas acho que quando você está convertendo entre tipos de exceção, geralmente sabe que os detalhes da primeira exceção não importam para o usuário externo. Ou seja, se houver __getattr__uma exceção, o erro provavelmente será um erro de digitação no acesso ao atributo, não um erro de implementação no código da classe atual. Mostrar a exceção anterior como contexto pode atrapalhar isso. E mesmo quando você suprime o contexto raise Whatever from None, ainda é possível obter a exceção anterior, se necessário via ex.__context__.
Blckknght
1
Eu queria aceitar sua resposta, no entanto, na pergunta, fiquei mais curioso se o uso de bloco try / catch aninhado é uma boa prática. Por outro lado, é a solução mais elegante e vou usá-la no meu código. Muito obrigado Martin.
quer
Michal: De nada. Também é mais rápido do que usar __getattribute__().
martineau
4

No Python, é mais fácil pedir perdão do que permissão. Não se preocupe com o tratamento de exceções aninhadas.

(Além disso, has*quase sempre usa exceções nos bastidores).

Ignacio Vazquez-Abrams
fonte
4

De acordo com a documentação , é melhor lidar com várias exceções por meio de tuplas ou assim:

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise
Blairg23
fonte
2
Esta resposta realmente não aborda a pergunta original, mas para quem a lê, observe o "vazio", exceto no final, é uma péssima idéia (geralmente), pois ela captura tudo, incluindo, por exemplo, NameError & KeyboardInterrupt - que geralmente não é o que você quis dizer!
perdeu
Dado que o código gera novamente a mesma exceção logo após a declaração de impressão, isso é realmente um grande problema. Nesse caso, ele pode fornecer mais contexto sobre a exceção sem ocultá-la. Se não re-aumentar, eu concordo totalmente, mas não acho que exista o risco de ocultar uma exceção que você não pretendia.
NimbusScale
4

Um exemplo bom e simples para try / except aninhado pode ser o seguinte:

import numpy as np

def divide(x, y):
    try:
        out = x/y
    except:
        try:
            out = np.inf * x / abs(x)
        except:
            out = np.nan
    finally:
        return out

Agora tente várias combinações e você obterá o resultado correto:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[é claro que estamos entorpecidos e não precisamos criar essa função]

Devastação
fonte
2

Uma coisa que eu gosto de evitar é criar uma nova exceção ao lidar com uma antiga. Isso torna as mensagens de erro confusas para serem lidas.

Por exemplo, no meu código, escrevi originalmente

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    raise KeyError(key)

E eu recebi esta mensagem.

>>> During handling of above exception, another exception occurred.

O que eu queria era isso:

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    pass
raise KeyError(key)

Não afeta como as exceções são tratadas. Em qualquer bloco de código, um KeyError teria sido capturado. Isso é apenas uma questão de obter pontos de estilo.

Steve Zelaznik
fonte
Veja o uso de raise da resposta aceita from Nonepara obter ainda mais pontos de estilo. :)
Pianossauro
1

Se tentar, exceto, finalmente, estiver aninhado dentro do bloco finalmente, o resultado de "filho" finalmente será preservado. Ainda não encontrei a expainação oficial, mas o seguinte trecho de código mostra esse comportamento no Python 3.6.

def f2():
    try:
        a = 4
        raise SyntaxError
    except SyntaxError as se:
        print('log SE')
        raise se from None
    finally:
        try:
            raise ValueError
        except ValueError as ve:
            a = 5
            print('log VE')
            raise ve from None
        finally:
            return 6       
        return a

In [1]: f2()
log SE
log VE
Out[2]: 6
Guanghua Shu
fonte
Esse comportamento é diferente do exemplo dado por @ Sławomir Lenart quando finalmente está aninhado dentro, exceto no bloco.
Guanghua Shu
0

Não acho que seja uma questão de ser pitônico ou elegante. É uma questão de evitar exceções, tanto quanto possível. Exceções destinam-se a manipular erros que podem ocorrer em código ou eventos sobre os quais você não tem controle. Nesse caso, você tem controle total ao verificar se um item é um atributo ou um dicionário, portanto, evite exceções aninhadas e continue com sua segunda tentativa.

owobeid
fonte
Dos documentos: em um ambiente multithread, a abordagem LBYL (Olhe antes de saltar ) pode arriscar a introdução de uma condição de corrida entre "a aparência" e "o salto". Por exemplo, o código if key no mapeamento: return mapping [key] pode falhar se outro thread remover a chave do mapeamento após o teste, mas antes da pesquisa. Esse problema pode ser resolvido com bloqueios ou usando a abordagem EAFP (mais fácil pedir perdão do que permissão) .
Nuno André