2008-10-28 7 views
18

J'ai deux itérateurs, un list et un objet itertools.count (c'est-à-dire un générateur de valeur infinie). Je voudrais fusionner ces deux dans un iterator résultant qui alterne les valeurs de rendement entre les deux:Comment fusionner deux itérateurs python?

>>> import itertools 
>>> c = itertools.count(1) 
>>> items = ['foo', 'bar'] 
>>> merged = imerge(items, c) # the mythical "imerge" 
>>> merged.next() 
'foo' 
>>> merged.next() 
1 
>>> merged.next() 
'bar' 
>>> merged.next() 
2 
>>> merged.next() 
Traceback (most recent call last): 
    ... 
StopIteration 

Quelle est la plus simple et la plus concise de le faire?

+0

Ne pas utiliser celui-ci: les gens 'liste ((rendement suivant (c)) ou i pour i pièces) ' –

Répondre

36

Un générateur résoudra bien votre problème.

def imerge(a, b): 
    for i, j in itertools.izip(a,b): 
     yield i 
     yield j 
+10

Vous devez ajouter un avertissement - cela ne fonctionnera que si la liste a est finie. – Claudiu

+0

itertools d'importation def iMerge (a, b): pour i, j dans zip (a, b): rendement i j rendement c = itertools.count (1) articles = [ 'foo', 'bar'] pour moi dans imerge (c, articles): imprimer i J'essaie cela, et cela fonctionne toujours. len (liste compressée) = min (l1, l2). La restriction de longueur finie n'est donc pas requise. – Pramod

+2

Claudiu a raison. Essayez de compresser deux générateurs infinis - vous manquerez de mémoire par la suite. Je préférerais utiliser itertools.izip au lieu de zip. Ensuite, vous construisez le zip que vous allez, au lieu de tout à la fois. Vous devez toujours faire attention aux boucles infinies, mais bon. –

10

Je ferais quelque chose comme ça. Ce sera le plus efficace dans le temps et l'espace, puisque vous n'aurez pas la surcharge des objets compressés ensemble. Cela fonctionnera également si a et b sont infinis.

def imerge(a, b): 
    i1 = iter(a) 
    i2 = iter(b) 
    while True: 
     try: 
      yield i1.next() 
      yield i2.next() 
     except StopIteration: 
      return 
+0

L'essai/excepté ici casse le protocole d'itérateur en étouffant le StopIteration, n'est-ce pas? –

+0

@David Eyk: c'est OK, car le retour d'un générateur augmente StopIteration de toute façon. La déclaration try dans ce cas est superflue. – efotinis

+0

@efotinis: Je ne le savais pas. Merci! –

7

Vous pouvez utiliser zip ainsi que itertools.chain. Cela ne fonctionne si la première liste est finie:

merge=itertools.chain(*[iter(i) for i in zip(['foo', 'bar'], itertools.count(1))]) 
+1

Pourquoi avez-vous une restriction sur la taille de la première liste? – Pramod

+5

Cela n'a pas besoin d'être si compliqué, cependant: 'merged = chain.from_iterable (izip (items, count (1)))' le fera. – intuited

15

Vous pouvez faire quelque chose qui est presque exaclty ce que @Pramod d'abord suggéré. L'avantage de cette approche est que vous ne manquerez pas de mémoire si a et b sont infinis.

+0

Tout à fait correct, David. @Pramod a changé sa réponse pour utiliser izip avant que j'aie remarqué le vôtre, mais merci! –

0

Pourquoi itertools est-il nécessaire?

def imerge(a,b): 
    for i,j in zip(a,b): 
     yield i 
     yield j 

Dans ce cas, au moins un ou b doit être de longueur finie, la cause zip retourne une liste, pas un itérateur. Si vous avez besoin d'un itérateur en sortie, vous pouvez opter pour la solution Claudiu.

+0

Je préfère un itérateur, car je lis des valeurs à partir de fichiers de taille arbitraire. Je suis sûr qu'il y a des cas où le zip est supérieur. –

3

Je ne suis pas sûr de ce que votre application est, mais vous pourriez trouver la fonction enumerate() plus utile.

>>> items = ['foo', 'bar', 'baz'] 
>>> for i, item in enumerate(items): 
... print item 
... print i 
... 
foo 
0 
bar 
1 
baz 
2 
+0

J'oublie toujours d'énumérer! Quel petit outil utile, même si cela ne fonctionnera pas dans mon application particulière. Merci! –

9

Je suis également d'accord que itertools n'est pas nécessaire.

Mais pourquoi arrêter à 2?

def tmerge(*iterators): 
    for values in zip(*iterators): 
     for value in values: 
     yield value 

gère un nombre quelconque d'itérateurs à partir de 0 vers le haut.

MISE À JOUR: DOH! Un commentateur a fait remarquer que cela ne fonctionnera que si tous les itérateurs ont la même longueur.

Le code est correct:

def tmerge(*iterators): 
    empty = {} 
    for values in itertools.izip_longest(*iterators, fillvalue=empty): 
    for value in values: 
     if value is not empty: 
     yield value 

et oui, je viens d'essayer avec des listes de longueur inégale, et une liste contenant {}.

+0

Est-ce que cela élimine chaque itérateur? Je pense que le zip tronquera au plus court. Je cherche une fusion qui en prend une de chaque itérateur, jusqu'à ce que chacun d'entre eux soit épuisé. –

+0

Comme c'est embarrassant. Tu es parfaitement correct! Voir mon code amélioré ici. –

+1

Aucun embarras nécessaire, votre réponse et réponse rapide m'a sauvé des heures de douleur! –

0

En utilisant itertools.izip(), au lieu de zip() comme dans certaines des autres réponses, permettra d'améliorer les performances:

Comme "pydoc itertools.izip" montre: « fonctionne comme la fonction zip() mais consomme moins de mémoire en retournant un itérateur au lieu d'une liste. "

Itertools.izip fonctionnera aussi correctement même si l'un des itérateurs est infini.

1

utilisation IZIP et enchaîner:

>>> list(itertools.chain.from_iterable(itertools.izip(items, c))) # 2.6 only 
['foo', 1, 'bar', 2] 

>>> list(itertools.chain(*itertools.izip(items, c))) 
['foo', 1, 'bar', 2] 
0

Procédé concise est d'utiliser une expression du générateur avec itertools.cycle(). Cela évite de créer une longue chaîne() de tuples.

generator = (it.next() for it in itertools.cycle([i1, i2])) 
3

Je préfère cette autre façon qui est beaucoup plus concise:

iter = reduce(lambda x,y: itertools.chain(x,y), iters) 
3

L'une des caractéristiques moins bien connues de Python est que vous pouvez avoir plus de clauses dans une expression de générateur. Très utile pour aplatir les listes imbriquées, comme celles que vous obtenez à partir de zip()/izip().

def imerge(*iterators): 
    return (value for row in itertools.izip(*iterators) for value in row) 
+1

Cela fonctionnerait certainement, bien que je trouve les expressions génératrices imbriquées moins lisibles. J'utiliserais ce style si j'étais inquiet au sujet de la performance. –

+0

Il est vraiment concis, comme le fait souvent Python, mais comment peut-on commencer à voir ce que fait ce code? Quel est l'effet de 'value for row in ... 'suivi de' for value in row'? N'est-ce pas un générateur de liste-imbriqué imbriqué? ne devrait-il pas se terminer par quelque chose comme 'pour rowvalue in row' ou est-ce que' value' est ombré? –

+0

@StevenLu Fondamentalement, il s'agit de deux boucles imbriquées, comme ceci: 'pour la ligne dans itertools.izip (* itérateurs): pour la valeur dans la ligne: valeur de rendement' –

3

Voici une solution élégante:

def alternate(*iterators): 
    while len(iterators) > 0: 
     try: 
      yield next(iterators[0]) 
      # Move this iterator to the back of the queue 
      iterators = iterators[1:] + iterators[:1] 
     except StopIteration: 
      # Remove this iterator from the queue completely 
      iterators = iterators[1:] 

En utilisant une file d'attente réelle pour une meilleure performance (comme le suggère David):

from collections import deque 

def alternate(*iterators): 
    queue = deque(iterators) 
    while len(queue) > 0: 
     iterator = queue.popleft() 
     try: 
      yield next(iterator) 
      queue.append(iterator) 
     except StopIteration: 
      pass 

Il fonctionne même lorsque certains itérateurs sont limitées et d'autres sont infinis:

from itertools import count 

for n in alternate(count(), iter(range(3)), count(100)): 
    input(n) 

Prints:

0 
0 
100 
1 
1 
101 
2 
2 
102 
3 
103 
4 
104 
5 
105 
6 
106 

Il a également arrête correctement si/lorsque tous les itérateurs ont été épuisés.

Si vous voulez gérer iterables non iterator, comme les listes, vous pouvez utiliser

def alternate(*iterables): 
    queue = deque(map(iter, iterables)) 
    ... 
+0

Une approche intéressante. :) Tant de façons de le faire. Je me demande si une rotation 'deque()' serait plus efficace que de reconstruire le tuple à chaque itération? –

+0

@DavidEyk Cela semble être une bonne idée. – user76284

+0

Remarquable. Encore mieux que la recette docs itertools. – Dubslow

Questions connexes