Python Language Evite operaciones repetitivas y costosas usando cláusula condicional


Ejemplo

Considere la siguiente lista de comprensión:

>>> 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, ...]

Esto da como resultado dos llamadas a f(x) para 1,000 valores de x : una llamada para generar el valor y la otra para verificar la condición if . Si f(x) es una operación particularmente costosa, esto puede tener implicaciones significativas en el rendimiento. Peor aún, si llamar a f() tiene efectos secundarios, puede tener resultados sorprendentes.

En su lugar, debe evaluar la operación costosa solo una vez para cada valor de x generando un iterable intermedio ( expresión del generador ) de la siguiente manera:

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

O, usando el mapa incorporado equivalente:

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

Otra forma que podría resultar en un código más legible es colocar el resultado parcial ( v en el ejemplo anterior) en un iterable (como una lista o una tupla) y luego iterar sobre él. Como v será el único elemento en el iterable, el resultado es que ahora tenemos una referencia a la salida de nuestra función lenta calculada solo una vez:

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

Sin embargo, en la práctica, la lógica del código puede ser más complicada y es importante mantenerlo legible. En general, se recomienda una función de generador por separado en un complejo de una sola línea:

>>> 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, ...]

Otra manera de evitar el cálculo de f(x) varias veces es utilizar el @functools.lru_cache() (Python 3.2+) decorador en f(x) . De esta manera, dado que la salida de f para la entrada x ya se ha calculado una vez, la invocación de la segunda función de la comprensión de la lista original será tan rápida como la búsqueda de un diccionario. Este enfoque utiliza la memoria para mejorar la eficiencia, que es comparable al uso de expresiones generadoras.


Di que tienes que aplanar una lista

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

Algunos de los métodos podrían ser:

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

sum(l, [])

list(itertools.chain(*l))

Sin embargo, la comprensión de la lista proporcionaría la mejor complejidad de tiempo.

[item for sublist in l for item in sublist]

Los accesos directos basados ​​en + (incluido el uso implícito en la suma) son, por necesidad, O (L ^ 2) cuando hay L sublistas: a medida que la lista de resultados intermedios se hace más larga, en cada paso se obtiene un nuevo objeto de lista de resultados intermedios. asignados, y todos los elementos en el resultado intermedio anterior deben copiarse (así como algunos nuevos agregados al final). Entonces (por simplicidad y sin pérdida de generalidad real) digamos que tiene L sublistas de I elementos cada uno: los primeros elementos I se copian una y otra vez L-1 veces, el segundo I elementos L-2 veces, y así sucesivamente; el número total de copias es I veces la suma de x para x de 1 a L excluidas, es decir, I * (L ** 2) / 2.

La lista de comprensión solo genera una lista, una vez, y copia cada elemento (de su lugar de residencia original a la lista de resultados) también una sola vez.