I tentativi nidificati / esclusi i blocchi in Python sono una buona pratica di programmazione?


201

Sto scrivendo il mio contenitore, che deve consentire l'accesso a un dizionario all'interno tramite chiamate di attributi. L'uso tipico del contenitore sarebbe come questo:

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

So che potrebbe essere stupido scrivere qualcosa del genere, ma questa è la funzionalità che devo fornire. Stavo pensando di implementarlo nel modo seguente:

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"

Non sono sicuro se la nidificazione di tentativi / eccezioni sia una buona pratica, quindi un altro modo sarebbe usare 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")

O per usarne uno e provare a catturare il blocco in questo modo:

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

Quale opzione è più pitonica ed elegante?


Gradirei la fornitura di divinità pitone if 'foo' in dict_container:. Amen.
gseattle,

Risposte:


180

Il tuo primo esempio va benissimo. Anche i documenti ufficiali di Python raccomandano questo stile noto come EAFP .

Personalmente, preferisco evitare l'annidamento quando non è necessario:

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()è stato deprecato a lungo in Python 2. Utilizzare item in self.dictinvece.


2
return object.__getattribute__(item)non è corretto e produrrà un TypeErrorperché viene passato il numero errato di argomenti. Dovrebbe invece essere return object.__getattribute__(self, item).
martineau,

13
PEP 20: flat è meglio di nidificato.
Ioannis Filippidis,

7
Cosa significa il from Nonesignificato nell'ultima riga?
niklas,

2
@niklas In sostanza sopprime il contesto delle eccezioni ("durante la gestione di questa eccezione si è verificata un'altra eccezione"). Vedi qui
Kade,

Il fatto che i documenti di Python raccomandino di annidare try è un po 'folle. È ovviamente uno stile orrendo. Il modo corretto di gestire una catena di operazioni in cui le cose possono fallire in quel modo sarebbe usare una sorta di costrutto monadico, che Python non supporta.
Henry Henrinson,

19

Mentre in Java è davvero una cattiva pratica usare le eccezioni per il controllo del flusso (principalmente perché le eccezioni costringono la jvm a raccogliere risorse ( più qui )), in Python hai 2 principi importanti: Duck Typing ed EAFP . Ciò significa fondamentalmente che sei incoraggiato a provare a usare un oggetto nel modo in cui pensi che funzioni, e a gestire quando le cose non sono così.

In sintesi, l'unico problema sarebbe che il tuo codice fosse troppo rientrato. Se ne hai voglia, prova a semplificare alcuni degli annidamenti come suggerito da lqc


10

Per il tuo esempio specifico, in realtà non è necessario nidificarli. Se l'espressione nel tryblocco ha esito positivo, la funzione tornerà, quindi qualsiasi codice dopo l'intero blocco try / tranne verrà eseguito solo se il primo tentativo fallisce. Quindi puoi semplicemente fare:

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"

Annidarli non è male, ma sento che lasciarlo piatto rende la struttura più chiara: stai provando in sequenza una serie di cose e restituendo la prima che funziona.

Per inciso, potresti voler pensare se vuoi davvero usare __getattribute__invece di __getattr__qui. L'uso __getattr__semplifica le cose perché saprai che il normale processo di ricerca degli attributi è già fallito.


10

Fai solo attenzione: in questo caso finallyviene prima toccato MA anche saltato.

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

Caspita mi fa impazzire ... Potresti indicarmi un frammento di documentazione che spiega questo comportamento?
Michal,

1
@Michal: fyi: finallyvengono eseguiti entrambi i blocchi a(0), ma finally-returnviene restituito solo il genitore .
Sławomir Lenart,

7

Secondo me questo sarebbe il modo più pitone per gestirlo, anche se e perché fa discutere la tua domanda. Si noti che questo definisce __getattr__()invece che __getattribute__()perché farlo significa che deve solo gestire gli attributi "speciali" che devono essere mantenuti nel dizionario 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")

2
Nota che sollevare un'eccezione in un exceptblocco può dare un output confuso in Python 3. Questo perché (per PEP 3134) Python 3 tiene traccia della prima eccezione (il KeyError) come "contesto" della seconda eccezione (il AttributeError) e se raggiunge il livello superiore, stamperà un traceback che include entrambe le eccezioni. Ciò può essere utile quando non si prevedeva una seconda eccezione, ma se si genera deliberatamente la seconda eccezione, è indesiderabile. Per Python 3.3, PEP 415 ha aggiunto la possibilità di sopprimere il contesto utilizzando raise AttributeError("whatever") from None.
Blckknght,

3
@Blckknght: la stampa di un traceback che include entrambe le eccezioni andrebbe bene in questo caso. In altre parole, non penso che la tua affermazione generale che sia sempre indesiderabile sia vera. Nell'uso qui, sta trasformando a KeyErrorin AttributeErrore mostrando che quello che è successo in un traceback sarebbe utile e appropriato.
martineau,

Per situazioni più complicate potresti avere ragione, ma penso che quando stai convertendo tra tipi di eccezione sai spesso che i dettagli della prima eccezione non contano per l'utente esterno. Cioè, se __getattr__solleva un'eccezione, il bug è probabilmente un errore di battitura nell'accesso agli attributi, non un bug di implementazione nel codice della classe corrente. Mostrare l'eccezione precedente come contesto può confonderla. E anche quando si sopprime il contesto con raise Whatever from None, è ancora possibile ottenere l'eccezione precedente, se necessario tramite ex.__context__.
Blckknght,

1
Volevo accettare la tua risposta, tuttavia nella domanda ero più curioso di sapere se usare o meno il blocco try / catch nidificato fosse una buona pratica. D'altra parte è la soluzione più elegante e la userò nel mio codice. Mille grazie Martin.
Michal,

Michal: Prego. È anche più veloce dell'uso __getattribute__().
martineau,

4

In Python è più facile chiedere perdono che autorizzazione. Non perdere la gestione delle eccezioni nidificate.

(Inoltre, has*usa quasi sempre delle eccezioni sotto la copertina.)


4

Secondo la documentazione , è meglio gestire più eccezioni tramite le tuple o in questo modo:

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

2
Questa risposta non affronta davvero la domanda originale, ma per chiunque la legga notare il "nudo" tranne che alla fine è un'idea terribile (di solito) in quanto catturerà tutto compreso ad esempio NameError & KeyboardInterrupt - che di solito non è quello che volevi dire!
Perso il

Dato che il codice genera nuovamente la stessa eccezione subito dopo l'istruzione print, è davvero un grosso problema. In questo caso può fornire più contesto sull'eccezione senza nasconderlo. Se non avesse rilanciato, sarei totalmente d'accordo, ma non credo che ci sia il rischio di nascondere un'eccezione che non intendevi.
NimbusScale l'

4

Un esempio valido e semplice per provare / tranne nidificato potrebbe essere il seguente:

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

Ora prova varie combinazioni e otterrai il risultato corretto:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[ovviamente abbiamo intorpidimento, quindi non è necessario creare questa funzione]


2

Una cosa che mi piace evitare è sollevare una nuova eccezione mentre ne gestisco una vecchia. Rende confusi i messaggi di errore da leggere.

Ad esempio, nel mio codice, ho originariamente scritto

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

E ho ricevuto questo messaggio.

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

Quello che volevo era questo:

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

Non influisce sulla gestione delle eccezioni. In entrambi i blocchi di codice, sarebbe stato rilevato un KeyError. Questo è semplicemente un problema per ottenere punti di stile.


Vedi l'uso accettato del rilancio della risposta accettata from None, per ancora più punti stile. :)
Pianosaurus,

1

Se try-salvo-finally è nidificato all'interno del blocco finally, il risultato di "child" è infine conservato. Non ho ancora trovato la scadenza ufficiale, ma il seguente frammento di codice mostra questo comportamento in 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

Questo comportamento è diverso dall'esempio fornito da @ Sławomir Lenart quando alla fine viene annidato all'interno tranne il blocco.
Guanghua Shu,

0

Non penso sia una questione di essere pitonica o elegante. Si tratta di prevenire le eccezioni il più possibile. Le eccezioni hanno lo scopo di gestire gli errori che potrebbero verificarsi nel codice o eventi su cui non si ha alcun controllo. In questo caso hai il pieno controllo quando controlli se un elemento è un attributo o in un dizionario, quindi evita le eccezioni nidificate e mantieni il secondo tentativo.


Dai documenti: in un ambiente multi-thread, l'approccio LBYL (Look Before You Leap) può rischiare di introdurre una condizione di competizione tra "lo sguardo" e "il salto". Ad esempio, il codice, se chiave nella mappatura: return mapping [chiave] può fallire se un altro thread rimuove la chiave dalla mappatura dopo il test, ma prima della ricerca. Questo problema può essere risolto con i lucchetti o utilizzando l'approccio EAFP (Più facile chiedere perdono che permesso) .
Nuno André,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.