Modifica comportamento - Caso speciale in un ciclo

2

Ho un modulo come di seguito:

def normalize(sub_item):
    pass

def process(sub_item):
    pass

def cleanup(sub_item):
    pass

def calculator(item):
    pass

def main(item):
    for index, sub_item in enumerate(item.sub_items):
        normalize(sub_item)
        process(sub_item)

        if index == 0:
            calculator(sub_item)

        cleanup(sub_item)

Ho una logica speciale per il primo sub_item ma questa logica può essere applicata solo dopo il passo normalize . In tutti i casi [Educed quess-Editor] il passo cleanup rimuoverà la risorsa dal sistema. Aggiungere una dichiarazione condizionale è facile ma non mi sembra pulito. Qualche idea?

    
posta haitran 24.08.2018 - 09:04
fonte

2 risposte

2

... the logic can only be applied after the normalize step and the cleanup step will remove the resource from the system.

Hai delle regole che non sono perfettamente pulite, il che significa che il codice che le implementa non lo sarà nemmeno. Tutto ha dei compromessi e non ci sono regole tagliate e asciutte su quali siano le migliori da fare.

Mettere la decisione all'interno del ciclo ha un vantaggio di leggibilità perché mantiene le regole in un unico posto: normalizza, elabora, calcola se il primo elemento, pulizia. Si paga per dover prendere una decisione in ogni iterazione del ciclo. Se si può o meno tollerare ciò dipende dal fatto che l'overhead di iterazione aggiuntivo interromperà il budget delle prestazioni. Se stai facendo una dozzina di articoli, potrebbe non fare la differenza, ma se stai facendo un miliardo o due, potrebbe. Questo è qualcosa che devi misurare e valutare in base alla tua situazione.

Se il costo di prendere ripetutamente la decisione è troppo alto, puoi fare quello che farebbe un compilatore con un ottimo ottimizzatore e srotolare solo la prima iterazione del ciclo:

# Requirement ABC123 says the first item gets special treatment.
first_item = item[0]
normalize(first_item)
process(first_item)
calculator(first_item)  # Special treatment
cleanup(first_item)

for index, sub_item in enumerate(item[1:]):
    normalize(sub_item)
    process(sub_item)
    cleanup(sub_item)

(Sto trattando item come un elenco per mantenere l'esempio semplice, ma puoi fare lo stesso con un iteratore.)

Questo ti risparmia l'impatto sul rendimento di prendere la decisione ogni volta a spese di dover ripetere la maggior parte del processo altrove nel codice. Se il codice del mondo reale è così semplice e tutto è contenuto in dieci righe come in questo caso, non è un grosso problema in termini pratici. Alcuni dei rischi posti dalla bruttezza addizionale possono essere mitigati attraverso la documentazione nel codice:

# WARNING:
#
# This is done separately to meet the performance limit
# set out in requirement OU812.  If the non-special part
# of the algorithm changes, make sure both versions are
# altered at the same time.

...Process first item...

# See warning above about making modifications.

for index, sub_item in enumerate(item[1:]):
    ...
    
risposta data 24.08.2018 - 15:01
fonte
1

Se devi eseguire sempre una pulizia, prendi in considerazione l'utilizzo di un gestore del contesto :

from contextlib import contextmanager

@contextmanager
def normalized(sub_item):
  ... # normalize
  yield resource
  ... # cleanup

Se la pulizia è necessaria anche con un'eccezione:

@contextmanager
def normalized(sub_item):
  ... # normalize
  try:
    yield resource
  finally:
    ... # cleanup

È possibile modificare le altre funzioni in modo che richiedano la risorsa dal contesto come argomento, impedendo che vengano richiamate al di fuori di tale contesto. Poi:

for index, sub_item in enumerate(item.sub_items):
    with normalized(sub_item) as resource:
        process(sub_item)

        if index == 0:
            calculator(sub_item, resource)

I.e., usa le dipendenze del flusso di dati per imporre un particolare ordine. Questo non è perfetto in Python perché l'ambito della variabile resource sarà piuttosto ampio, ma è molto più difficile fare un errore accidentale.

Il test per index == 0 è il modo più pulito per aggiungere un'ulteriore elaborazione per il primo elemento. Esistono alternative come l'utilizzo esplicito di iteratori, ma implicano una certa quantità di duplicazione del codice e sono più difficili da leggere:

sub_items_iter = iter(item.sub_items)

# consume first item from iterator
for sub_item in sub_items_iter:
  with normalized(sub_item) as resource:
    process(sub_item)
    calculator(sub_item, resource)
  break

# consume remaining items
for sub_item in sub_items_iter:
  with normalized(sub_item) as resource:
    process(sub_item)

L'uso del ciclo for per consumare un singolo elemento da un iteratore non è ovvio, ma è ancora più chiaro della variante di de-sugared:

try:
  sub_item = next(sub_items_iter)
  has_first = True
except StopIteration:
  has_first = False

if has_first:
  with normalized(sub_item) as resource:
    process(sub_item)
    calculator(sub_item, resource)
    
risposta data 24.08.2018 - 10:27
fonte

Leggi altre domande sui tag