Python Language Evita operazioni ripetitive e costose usando la clausola condizionale


Esempio

Considera la sotto lista di comprensione:

>>> def f(x):
...     import time
...     time.sleep(.1)       # Simulate expensive function
...     return x**2

>>> [f(x) for x in range(1000) if f(x) > 10]
[16, 25, 36, ...]

Ciò comporta due chiamate a f(x) per 1.000 valori di x : una chiamata per la generazione del valore e l'altra per il controllo della condizione if . Se f(x) è un'operazione particolarmente costosa, ciò può avere implicazioni significative sulle prestazioni. Peggio ancora, se chiamare f() ha effetti collaterali, può avere risultati sorprendenti.

Invece, dovresti valutare l'operazione costosa solo una volta per ogni valore di x generando un iterabile intermedio ( espressione del generatore ) come segue:

>>> [v for v in (f(x) for x in range(1000)) if v > 10]
[16, 25, 36, ...]

Oppure, usando l'equivalente della mappa integrata:

>>> [v for v in map(f, range(1000)) if v > 10]
[16, 25, 36, ...]

Un altro modo che potrebbe risultare in un codice più leggibile consiste nel mettere il risultato parziale ( v nell'esempio precedente) in un iterabile (come un elenco o una tupla) e quindi scorrere su di esso. Dato che v sarà l'unico elemento nel iterabile, il risultato è che ora abbiamo un riferimento all'output della nostra funzione lenta calcolata una sola volta:

>>> [v for x in range(1000) for v in [f(x)] if v > 10]
[16, 25, 36, ...]

Tuttavia, in pratica, la logica del codice può essere più complicata ed è importante tenerlo leggibile. In generale, una funzione di generatore separata è raccomandata su un one-liner complesso:

>>> def process_prime_numbers(iterable):
...     for x in iterable:
...         if is_prime(x):
...             yield f(x)
...
>>> [x for x in process_prime_numbers(range(1000)) if x > 10]
[11, 13, 17, 19, ...]

Un altro modo per evitare che il calcolo f(x) più volte è quello di utilizzare il @functools.lru_cache() (Python 3.2+) decoratore su f(x) . In questo modo, poiché l'output di f per l'input x è già stato calcolato una volta, la seconda funzione invocata dalla comprensione dell'elenco originale sarà veloce quanto una ricerca di dizionario. Questo approccio utilizza la memoizzazione per migliorare l'efficienza, che è paragonabile all'uso di espressioni generatrici.


Di 'che devi appiattire una lista

l = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]

Alcuni dei metodi potrebbero essere:

reduce(lambda x, y: x+y, l)

sum(l, [])

list(itertools.chain(*l))

Tuttavia la comprensione delle liste fornirebbe la migliore complessità temporale.

[item for sublist in l for item in sublist]

Le scorciatoie basate su + (incluso l'uso implicito in somma) sono, per necessità, O (L ^ 2) quando ci sono sottoliste L - poiché l'elenco dei risultati intermedi continua ad allungarsi, ad ogni passaggio viene ottenuto un nuovo elenco di risultati intermedi assegnato e tutti gli elementi nel risultato intermedio precedente devono essere copiati sopra (così come alcuni nuovi aggiunti alla fine). Quindi (per semplicità e senza effettiva perdita di generalità) dite di avere L Liste di elementi I ciascuno: i primi elementi I vengono copiati avanti e indietro L-1 volte, il secondo I elementi L-2 volte, e così via; il numero totale di copie è I volte la somma di x per x da 1 a L esclusa, cioè I * (L ** 2) / 2.

La comprensione dell'elenco genera solo una lista, una volta, e copia ogni oggetto (dalla sua posizione originale di residenza alla lista dei risultati) anche esattamente una volta.