Rendimiento de la anidación con bucles

En todo caso, aun cuando desde un punto de vista conceptual sea posible "mover" las condiciones (respetando siempre la regla de que la variable deba ser declarada antes que la condición) el rendimiento del código no es siempre el mismo.

Por ejemplo, partamos de las siguientes dos listas de diez mil valores aleatorios 0 o 1:

import random

a = random.choices([0, 1], k = 10000)
b = random.choices([0, 1], k = 10000)

Supongamos que queremos recorrerlas de forma anidada extrayendo las parejas de unos. Es decir, queremos replicar el comportamiento del siguiente código:

%%time
m = []
for x in a:
    for y in b:
        if (x == 1) & (y == 1):
            m.append((x, y))

Wall time: 12.8 s

len(m)

24879244

Comprobamos que el resultado está formado por casi 25 millones de tuplas.

Ahora creamos nuestra primera versión de la list comprehension:

%%time
c = [(x, y) for x in a for y in b if x == 1 if y == 1]

Wall time: 5.78 s

Tal y como está escrita (y tal y como se ha escrito el bucle for inicial) se recorren todos los valores de la lista "a", para cada uno de ellos se recorren todos los valores de la lista "b" y solo al final se aplican las condiciones.

Pero podríamos mejorarlo de la siguiente forma:

%%time
c = [(x, y) for x in a if x == 1 for y in b if y == 1]

Wall time: 3.63 s

len(c)

24879244

Ahora, recorremos la lista "a" y solo cuando toma el valor 1, recorremos la lista "b".

De hecho, podríamos preguntarnos si mejoraría el rendimiento de la primera versión de nuestra list comprehension con el siguiente código:

%%time
c = [(x, y) for x in a for y in b if (x == 1) and (y == 1)]

Wall time: 6.14 s

...pero comprobamos que no es así. En realidad, en ambas versiones estamos permitiendo a Python que termine la comprobación de las condiciones en cuanto la primera no se cumpla, de forma que habría que buscar en el código fuente la diferencia de rendimiento entre ambos enfoques.