È ragionevole scrivere codice di debug peggiore per migliorare il codice di produzione?

4

Il titolo parla praticamente da solo, ma fornirò l'attuale decisione che sto affrontando.

Sto migrando il codice Python verso l'uso di generatori. Il codice attuale ha il seguente aspetto:

...
l = returns_a_list(args)
log.debug('examining {} entries', len(l))
for e in l:
    do_stuff(e)
...

Al di fuori del log di debug, l si adatta molto bene al caso d'uso di un generatore e la sua lunghezza non è necessaria da nessun'altra parte. Tuttavia, a causa del registro di debug, l'utilizzo di un generatore sarebbe simile a questo:

...
log.debug('examining {} entries', sum(1 for _ in returns_a_generator(args))
for e in returns_a_generator(args):
    do_stuff(e)
...

Questo è meno leggibile e chiama il generatore due volte. Tuttavia, il codice di produzione è semplice. Un'altra opzione potrebbe essere:

...
count = 0
for e in returns_a_generator(args):
    do_stuff(e)
    count += 1
log.debug('examining {} entries', count)
...

Questo non chiama il generatore due volte, il che non è un grosso problema dato che non ci interessa davvero le prestazioni in modalità di debug. A mio avviso, sembra un po 'più semplice in termini di conteggio degli elementi rispetto a sum(1 for _ in generator) , che non trasmette le intenzioni in modo chiaro (tuttavia, il precedente messaggio di debug dovrebbe suggerire cosa sta facendo quel frammento). Tuttavia non sono ancora sicuro che lo spostamento del log sia accettabile (cosa succede se do_stuff fallisce? E se per qualche motivo il generatore restituisce la strada a molti valori? Mi piacerebbe avere quella linea di debug prima che il programma si blocchi o inizi a calcolare fino a quando la morte del calore dell'universo.). Inoltre, il conteggio viene eseguito anche senza registro di debug.

Quindi, qual è la tua opinione su questo problema generale? Cosa penseresti quando inciampi su una di quelle tre opzioni? Dopo averli capiti, ti interesserebbe che il registro di debug fosse contorto e file un problema / patch o penseresti "Ah, capito, è ragionevole così?"

EDIT: questa è la community dei programmatori. Non sono così interessato a soluzioni concrete a questo particolare problema, ma più a opinioni sulla prima, più ampia domanda (a meno che ovviamente non ci sia una buona argomentazione sul fatto che una tale scelta non dovrebbe mai accadere).

    
posta nathdwek 20.10.2016 - 09:43
fonte

3 risposte

5

In un caso generico: se la registrazione ti rende alcune operazioni non produttive che possono avere un impatto reale sulle prestazioni di produzione, avvolgi il tuo codice con codice come isDebugEnabled() (o direttiva #ifdef DEBUG o qualsiasi altra cosa), ad esempio:

l = returns_a_list(args)
if(logger.isDebugEnabled()){
    // your code
    log.debug('examining {} entries', len(l))
}
for e in l:
    do_stuff(e)

Usa solo il wrapping quando c'è un calcolo extra, un semplice log senza non averlo bisogno e renderà il tuo codice meno leggibile.

Un altro modo:

if(logger.isDebugEnabled()){
   l = returns_a_list(args);
    log.debug('examining {} entries', len(l))
}else{
    l = returns_a_generator(args);
}

for e in l:
    do_stuff(e)

Devi essere sicuro, tuttavia, che la modifica di returns_a_generator / returns_a_list non influisce sul comportamento del tuo codice, poiché non avere lo stesso ordine potrebbe avere un impatto su qualcosa.

Tuttavia, non usare la tua ultima soluzione per il debug, non aggiungendo codice come questo solo per il debug, perché la gente non è sicura che sia solo a scopo di debug a meno che tu non abbia dimenticato di inserire un commento.

Nota: non so quale sia la vera differenza tra returns_a_genrator / returns_a_list Quindi non so se il caso presentato non appartiene alla pre-ottimizzazione non necessaria (costo della lunghezza di calcolo generalmente nulla). Sto solo mostrando un uso standard considerando il campione dato.

EDIT Per concludere:

Apprezzo la leggibilità dei prodotti / la manutenibilità / logica su tutto ciò che riguarda il debug:

  • Non mescolo nessuna logica aggiuntiva per il debug del codice all'interno della logica della mia produzione. Ciò rende più difficile isolare ciò che è realmente necessario per la produzione e ciò che viene utilizzato per il debug. La tua terza opzione è una violazione di quella "regola".
  • Tutto il codice che si verifica nel wrapper if non modifica le variabili utilizzate dal codice prod. Quindi quando vedo un if(logger.isDebugEnabled) e sto verificando qualche codice problema con la produzione, non lo leggo nemmeno. Quindi sto leggendo velocemente quanto il codice di debug non era qui.
risposta data 20.10.2016 - 09:51
fonte
3

Quando qualcosa non è leggibile, spesso aiuta a incapsularlo in una funzione (o, a volte, in una classe). L'invocazione della funzione è leggibile (purché il nome sia significativo e l'API sia chiara) e la definizione della funzione dovrebbe essere leggibile perché ci si sta spostando verso il dominio di un problema più piccolo.

Nel tuo esempio, puoi creare una funzione che avvolgerà il generatore e registrerà il conteggio:

def iteration_logger(generator):
    count = 0
    for value in generator:
        yield value
        count += 1
    log.debug('examining {} entries', count)

for e in iteration_logger(returns_a_generator(args)):
    do_stuff(e)

In genere, altri casi possono essere estratti alla loro funzione.

Se scegli questo approccio, fai attenzione ad evitare di fare cose nelle istruzioni che la loro funzione di root è un logger. Ad esempio:

log.debug('do_stuff resulted in {}', do_stuff())

Questo è male perché quando si legge questo codice possono ignorare inconsciamente queste istruzioni perché sembra che tutto ciò che fanno è la registrazione. Qualcosa di simile, tuttavia, è OK:

result = logged('do_stuff resulted in {}', do_stuff())

Perché qui è chiaro che questa linea è più di una semplice registrazione - imposta una variabile, quindi deve fare qualche calcolo / ricerca.

    
risposta data 20.10.2016 - 13:52
fonte
2

Un'altra opzione: se devi solo gestire le lunghezze dei generatori, puoi utilizzare le conversioni di formato. Un esempio molto semplice: definiamo:

from string import Formatter


class MyFormatter(Formatter):
    def convert_field(self, value, conversion):
        try:
            return super().convert_field(value, conversion)
        except ValueError:
            if conversion == 'c':
                return sum(1 for _ in value)
            raise


class log:
    def __init__(self):
        self.formatter = MyFormatter()

    def debug(self, fmt, *args, **kwargs):
        print(self.formatter.format(fmt, *args, **kwargs))

    info = debug
log = log()

( log qui è super semplificato rispetto al tuo logger - serve solo a dimostrare conversioni in formato personalizzato)

Quindi puoi usare:

log.debug('examining {!c} entries', returns_a_generator(args))

for e in returns_a_generator(args):
    do_stuff(e)

Poiché utilizziamo {!c} , la nostra conversione personalizzata si attiverà e calcolerà la lunghezza del generatore, ma ciò si verificherà solo se la registrazione di debug è attiva. Se è spento, il generatore verrà creato ma non verrà eseguito.

    
risposta data 20.10.2016 - 14:47
fonte

Leggi altre domande sui tag