2017-07-13 7 views
7

Considérons la fonction suivante, dont la sortie est censé être le produit cartésien d'une séquence de iterables:Pourquoi ma fonction cartésienne ne fonctionne-t-elle pas?

def cart(*iterables): 
    out = ((e,) for e in iterables[0]) 
    for iterable in iterables[1:]: 
     out = (e1 + (e2,) for e1 in out for e2 in iterable) 
    return out 

fonctionne bien lorsque le générateur comprehensions sont remplacés par des list comprehensions. Fonctionne également quand il n'y a que 2 itérations. Mais quand j'essaie

print(list(cart([1, 2, 3], 'ab', [4, 5]))) 

Je reçois

[(1, 4, 4), (1, 4, 5), (1, 5, 4), (1, 5, 5), 
(2, 4, 4), (2, 4, 5), (2, 5, 4), (2, 5, 5), 
(3, 4, 4), (3, 4, 5), (3, 5, 4), (3, 5, 5)] 

Pourquoi et non le produit cartésien?

+0

Vous pouvez stocker des résultats intermédiaires en mémoire (comme l'approche par liste qui fonctionne) et ne pas différer leur évaluation avec cette génération. exp. dont les valeurs changent constamment à travers les itérations. –

+1

Je sais que cette question concerne l'implémentation de l'algorithme pour le produit cartésien en Python, mais au cas où quelqu'un finirait par chercher comment faire un produit cartésien en Python, notez que ceci est déjà implémenté dans ['itertools.product'] (https://docs.python.org/3/library/itertools.html#itertools.product). – jdehesa

Répondre

8

Vous créez expressions de générateur, qui ne sont pas répétées jusqu'à la prochaine itération de la boucle for iterable in iterables[1:]:. Ils utilisent les fermetures , qui sont recherchées au moment de l'exécution.

Les expressions de générateur sont essentiellement de petites fonctions à cet égard, elles créent leur propre portée, et tous les noms de la portée parent doivent être traités comme des fermetures pour que cela fonctionne. La 'fonction' est exécutée lorsque vous itérez, et seulement alors la fermeture est nécessaire et résolue à la valeur actuelle de la variable référencée.

Vous créez donc une expression de générateur comme ceci:

(e1 + (e2,) for e1 in out for e2 in iterable) 

iterable est une fermeture prise de la portée des parents (vos locaux de fonction). Mais la recherche ne se fait pas avant la prochaine itération lorsque vous bouclez, , auquel point iterable est l'élément suivant dans la séquence.

Donc, pour votre entrée [1, 2, 3], 'ab', [4, 5], vous créez une expression du générateur quand iterable = 'ab' mais au moment où vous itérer en fait, la boucle for a attribué une nouvelle valeur et est maintenant iterable = [4, 5]. Lorsque vous itérez enfin sur le générateur final (chaîné), seule la toute dernière affectation à iterable compte.

Vous créez effectivement un produit sur iterables[0], iterables[-1] * len(iterables) - 1; iterables[1] jusqu'à iterables[-2] sont complètement ignorés, tous remplacés par iterables[-1].

Vous pouvez utiliser un générateur fonction pour éviter le problème de fermeture, en passant iterable à être lié à un local:

def gen_step(out, iterable): 
    for e1 in out: 
     for e2 in iterable: 
      yield e1 + (e2,) 

def cart(*iterables): 
    out = ((e,) for e in iterables[0]) 
    for iterable in iterables[1:]: 
     out = gen_step(out, iterable) 
    return out 

Vous pouvez faire la même chose avec un lambda retour l'expression du générateur:

def cart(*iterables): 
    out = ((e,) for e in iterables[0]) 
    for iterable in iterables[1:]: 
     out = (lambda it=iterable: (e1 + (e2,) for e1 in out for e2 in it))() 
    return out 
+0

Les alternatives sont encore paresseuses. Agréable. –