Façon pythonique de combiner deux listes de manière alternée?

88

J'ai deux listes, dont la première est garantie de contenir exactement un élément de plus que le second . J'aimerais connaître la manière la plus pythonique de créer une nouvelle liste dont les valeurs d'index pair proviennent de la première liste et dont les valeurs d'index impairs proviennent de la deuxième liste.

# example inputs
list1 = ['f', 'o', 'o']
list2 = ['hello', 'world']

# desired output
['f', 'hello', 'o', 'world', 'o']

Cela fonctionne, mais ce n'est pas joli:

list3 = []
while True:
    try:
        list3.append(list1.pop(0))
        list3.append(list2.pop(0))
    except IndexError:
        break

Comment peut-on y parvenir autrement? Quelle est l'approche la plus pythonique?

davidchambers
la source
1
duplication possible de Alternance entre itérateurs en Python
Felix Kling
Pas un double! La réponse acceptée dans l'article lié ci-dessus produit une liste de tuples, pas une seule liste fusionnée.
Paul Sasik
@Paul: Oui, la réponse acceptée ne donne pas la solution complète. Lisez les commentaires et les autres réponses. La question est fondamentalement la même et les autres solutions peuvent être appliquées ici.
Felix Kling
3
@Felix: Je suis respectueusement en désaccord. C'est vrai, les questions sont dans le même quartier mais pas vraiment en double. Comme preuve vague, jetez un œil aux réponses potentielles ici et comparez-les avec l'autre question.
Paul Sasik
Découvrez-les: stackoverflow.com/questions/7529376/…
wordsforthewise

Réponses:

112

Voici une façon de le faire en découpant:

>>> list1 = ['f', 'o', 'o']
>>> list2 = ['hello', 'world']
>>> result = [None]*(len(list1)+len(list2))
>>> result[::2] = list1
>>> result[1::2] = list2
>>> result
['f', 'hello', 'o', 'world', 'o']
Duncan
la source
3
Merci, Duncan. Je ne savais pas qu'il était possible de spécifier une étape lors du tranchage. Ce que j'aime dans cette approche, c'est la façon dont elle se lit naturellement. 1. Faites une liste de la longueur correcte. 2. Rempli les index pairs avec le contenu de list1. 3. Remplissez les index impairs avec le contenu de list2. Le fait que les listes soient de longueurs différentes n'est pas un problème dans ce cas!
davidchambers
2
Je pense que cela ne fonctionne que lorsque len (list1) - len (list2) est égal à 0 ou 1.
xan
1
Si les listes sont de longueurs appropriées, cela fonctionne, sinon la question d'origine ne spécifie pas la réponse attendue. Il peut être facilement modifié pour gérer la plupart des situations raisonnables: par exemple, si vous voulez que des éléments supplémentaires soient ignorés, coupez simplement la liste la plus longue avant de commencer; si vous voulez que les éléments supplémentaires soient entrelacés avec None, assurez-vous simplement que le résultat est initialisé avec d'autres None; si vous voulez que des éléments supplémentaires viennent d'être ajoutés à la fin, faites comme pour les ignorer, puis ajoutez-les.
Duncan le
1
Moi aussi, je n'étais pas clair. Ce que j'essayais de faire valoir, c'est que la solution de Duncan, contrairement à bon nombre de celles énumérées, n'est pas compliquée par le fait que les listes sont de longueur inégale. Bien sûr, cela ne s'applique que dans une gamme limitée de situations, mais je préférerais une solution vraiment élégante qui fonctionne dans ce cas à une solution moins élégante qui fonctionne pour deux listes.
davidchambers
1
Vous pouvez utiliser (2 * len (list1) -1) au lieu de (len (list1) + len (list2)), aussi je préfère [0 :: 2] au lieu de [:: 2].
Lord British
50

Il y a une recette pour cela dans la itertoolsdocumentation :

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).next for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))

ÉDITER:

Pour la version de python supérieure à 3:

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    pending = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while pending:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            pending -= 1
            nexts = cycle(islice(nexts, pending))
David Z
la source
Je trouve cette voie plus compliquée que nécessaire. Il y a une meilleure option ci-dessous en utilisant zip_longest.
Dubslow
@Dubslow Pour ce cas particulier, oui, c'est probablement exagéré (comme je l'ai mentionné dans un commentaire ailleurs), à moins que vous n'y ayez déjà accès. Cela pourrait cependant présenter certains avantages dans d'autres situations. Cette recette n'a certainement pas été conçue pour ce problème, elle le résout simplement.
David Z
1
Pour info, vous devriez utiliser la recette dans la itertools documentation car .next()ne fonctionne plus.
john w.
1
@johnw. il faut utiliser __next__. Ce n'est pas écrit dans la documentation, j'ai donc proposé une modification de la réponse.
Marine Galantin
@Marine Je préfère en quelque sorte que vous veniez de modifier l'exemple de code existant, mais je peux résoudre ce problème moi-même. Merci d'avoir contribué!
David Z
31

Cela devrait faire ce que vous voulez:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']
Mark Byers
la source
J'ai beaucoup aimé votre réponse initiale. Même si cela ne répondait pas parfaitement à la question, c'était une manière élégante de fusionner deux listes de même longueur. Je suggère de le conserver, ainsi que la mise en garde concernant la longueur, dans votre réponse actuelle.
Paul Sasik
1
Si list1 était à la place ['f', 'o', 'o', 'd'], son élément final ('d') n'apparaîtrait pas dans la liste résultante (ce qui est tout à fait correct étant donné les spécificités de la question). C'est une solution élégante!
davidchambers
1
@Mark yep (j'ai voté pour), soulignant simplement les différences (et les limites si d'autres personnes veulent un comportement différent)
cobbal
4
+1 pour résoudre le problème énoncé, et pour le faire simplement aussi :-) J'ai pensé que quelque chose comme ça serait possible. Honnêtement, je pense que la roundrobinfonction est un peu exagérée pour cette situation.
David Z
1
Pour travailler avec des listes de toutes tailles, vous pouvez simplement ajouter ce qui reste dans les itérateurs au résultat:list(itertools.chain(map(next, itertools.cycle(iters)), *iters))
panda-34
29
import itertools
print [x for x in itertools.chain.from_iterable(itertools.izip_longest(list1,list2)) if x]

Je pense que c'est la manière la plus pythonique de le faire.

utilisateur942640
la source
3
Pourquoi ce n'est pas la réponse acceptée? C'est le plus court et le plus pythonique et fonctionne avec différentes longueurs de liste!
Jairo Vadillo
5
le nom de la méthode est zip_longest et non izip_longest
Jairo Vadillo
1
Le problème avec ceci est que la valeur de remplissage par défaut de zip_longest peut écraser les Nones qui sont * supposés être dans la liste. Je vais éditer dans une version modifiée pour résoudre ce problème
Dubslow
Remarque: Cela posera des problèmes si les listes contiennent des éléments avec une valeur False, ou même des choses qui ne seront évaluées que Falsepar l' ifexpression -expression, comme par exemple une 0ou une liste vide. Cela peut être (partiellement) évité par ce qui suit: [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]. Bien sûr, cela ne fonctionnera toujours pas si les listes contiennent des Noneéléments qui doivent être préservés. Dans ce cas, vous devez changer l' fillvalueargument de zip_longest, comme Dubslow l'a déjà suggéré.
der_herr_g
Nonele problème semble avoir disparu, du moins depuis Python 3.7.6 (je ne sais pas pour les anciennes versions). Si alt_chainest défini comme def alt_chain(*iters, fillvalue=None): return chain.from_iterable(zip_longest(*iters, fillvalue=fillvalue)), alors list(alt_chain([0, False, 1, set(), 3, 4], [0, None, 1, {}], fillvalue=99))renvoie correctement [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99].
paime le
17

Sans itertools et en supposant que l1 est 1 élément plus long que l2:

>>> sum(zip(l1, l2+[0]), ())[:-1]
('f', 'hello', 'o', 'world', 'o')

En utilisant itertools et en supposant que les listes ne contiennent aucun:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')
Zart
la source
C'est ma réponse préférée. C'est tellement concis.
mbomb007
@ anishtain4 zip prend des paires d'éléments que tuples de listes [(l1[0], l2[0]), (l1[1], l2[1]), ...]. sumconcatène les tuples ensemble: (l1[0], l2[0]) + (l1[1], l2[1]) + ...résultant en des listes entrelacées. Le reste de la ligne est juste un rembourrage de l1 avec un élément supplémentaire pour que la fermeture éclair fonctionne et tranche jusqu'à -1 pour se débarrasser de ce rembourrage.
Zart
izip_longest (zip_longest depuis python 3) n'a pas besoin de + [0] padding, il remplit implicitement None lorsque les longueurs des listes ne correspondent pas, tandis que filter(None, ...(pourrait utiliser à la boolplace, ou None.__ne__) supprime les fausses valeurs, y compris 0, None et les chaînes vides, donc la deuxième expression n'est pas strictement équivalente à la première.
Zart
La question est de savoir comment avez-vous fait sumcela? Quel est le rôle du deuxième argument ici? Dans les documentations, le deuxième argument est start.
anishtain4
La valeur par défaut de start est 0, et vous ne pouvez pas faire 0+ (certains, tuple), donc start est changé en tuple vide.
Zart
13

Je sais que les questions portent sur deux listes, l'une contenant un élément de plus que l'autre, mais j'ai pensé que je mettrais cela pour d'autres qui pourraient trouver cette question.

Voici la solution de Duncan adaptée pour fonctionner avec deux listes de tailles différentes.

list1 = ['f', 'o', 'o', 'b', 'a', 'r']
list2 = ['hello', 'world']
num = min(len(list1), len(list2))
result = [None]*(num*2)
result[::2] = list1[:num]
result[1::2] = list2[:num]
result.extend(list1[num:])
result.extend(list2[num:])
result

Cela produit:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 
mhost
la source
6

Si les deux listes ont la même longueur, vous pouvez faire:

[x for y in zip(list1, list2) for x in y]

Comme la première liste contient un élément supplémentaire, vous pouvez l'ajouter post hoc:

[x for y in zip(list1, list2) for x in y] + [list1[-1]]
Quelqu'un
la source
2
^ Cela devrait être la réponse, python est devenu plus pythonique au cours des 10 dernières années
Tian
5

Voici une seule ligne qui le fait:

list3 = [ item for pair in zip(list1, list2 + [0]) for item in pair][:-1]

Geai
la source
2
Cela fonctionne correctement mais me semble inélégant car il fait tellement pour réaliser quelque chose d'aussi simple. Je ne dis pas que cette approche est inefficace, simplement qu'elle n'est pas particulièrement facile à lire.
davidchambers
2
def combine(list1, list2):
    lst = []
    len1 = len(list1)
    len2 = len(list2)

    for index in range( max(len1, len2) ):
        if index+1 <= len1:
            lst += [list1[index]]

        if index+1 <= len2:
            lst += [list2[index]]

    return lst
tuer
la source
Soyez très prudent lorsque vous utilisez des arguments par défaut mutables. Cela ne renverra la réponse correcte que la première fois qu'il sera appelé, car lst sera réutilisé pour chaque appel par la suite. Ce serait mieux écrit comme lst = None ... si lst vaut None: lst = [], bien que je ne vois pas de raison impérieuse d'opter pour cette approche par rapport aux autres listées ici.
davidchambers
lst est défini dans la fonction, de même qu'une variable locale. Le problème potentiel est que list1 et list2 sont réutilisés chaque fois que vous utilisez la fonction, même si vous appelez la fonction avec des listes différentes. Voir docs.python.org/tutorial/…
blokeley
1
@blokeley: faux, serait réutilisé s'il était combiné (list1 = [...], list2 = [...])
killown
Lorsque cette solution a été publiée pour la première fois, sa première ligne a été lue def combine(list1, list2, lst=[]):, d'où mon commentaire. Au moment où j'ai soumis ce commentaire, cependant, killown avait apporté le changement nécessaire.
davidchambers
2

Celui-ci est basé sur la contribution de Carlos Valiente ci-dessus avec une option pour alterner les groupes de plusieurs éléments et s'assurer que tous les éléments sont présents dans la sortie:

A=["a","b","c","d"]
B=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]

def cyclemix(xs, ys, n=1):
    for p in range(0,int((len(ys)+len(xs))/n)):
        for g in range(0,min(len(ys),n)):
            yield ys[0]
            ys.append(ys.pop(0))
        for g in range(0,min(len(xs),n)):
            yield xs[0]
            xs.append(xs.pop(0))

print [x for x in cyclemix(A, B, 3)]

Cela entrelacera les listes A et B par groupes de 3 valeurs chacun:

['a', 'b', 'c', 1, 2, 3, 'd', 'a', 'b', 4, 5, 6, 'c', 'd', 'a', 7, 8, 9, 'b', 'c', 'd', 10, 11, 12, 'a', 'b', 'c', 13, 14, 15]
catpnced
la source
2

Il pourrait être un peu tard pour acheter un autre one-liner en python. Cela fonctionne lorsque les deux listes ont une taille égale ou inégale. Une chose qui ne vaut rien est que cela modifiera a et b. Si c'est un problème, vous devez utiliser d'autres solutions.

a = ['f', 'o', 'o']
b = ['hello', 'world']
sum([[a.pop(0), b.pop(0)] for i in range(min(len(a), len(b)))],[])+a+b
['f', 'hello', 'o', 'world', 'o']
Allen
la source
1

Ma prise:

a = "hlowrd"
b = "el ol"

def func(xs, ys):
    ys = iter(ys)
    for x in xs:
        yield x
        yield ys.next()

print [x for x in func(a, b)]
Carlos Valiente
la source
1

Voici une ligne unique utilisant des compréhensions de liste, sans autres bibliothèques:

list3 = [sub[i] for i in range(len(list2)) for sub in [list1, list2]] + [list1[-1]]

Voici une autre approche, si vous autorisez la modification de votre liste initiale1 par effet secondaire:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]
chernevik
la source
1
from itertools import chain
list(chain(*zip('abc', 'def')))  # Note: this only works for lists of equal length
['a', 'd', 'b', 'e', 'c', 'f']
Ken Seehart
la source
0

Arrêts sur le plus court:

def interlace(*iters, next = next) -> collections.Iterable:
    """
    interlace(i1, i2, ..., in) -> (
        i1-0, i2-0, ..., in-0,
        i1-1, i2-1, ..., in-1,
        .
        .
        .
        i1-n, i2-n, ..., in-n,
    )
    """
    return map(next, cycle([iter(x) for x in iters]))

Bien sûr, la résolution de la méthode next / __ next__ peut être plus rapide.

jwp
la source
0

Ceci est méchant mais fonctionne quelle que soit la taille des listes:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]
chevalier araignée
la source
0

Plusieurs one-liners inspirés des réponses à une autre question :

import itertools

list(itertools.chain.from_iterable(itertools.izip_longest(list1, list2, fillvalue=object)))[:-1]

[i for l in itertools.izip_longest(list1, list2, fillvalue=object) for i in l if i is not object]

[item for sublist in map(None, list1, list2) for item in sublist][:-1]
mots pour
la source
0

Que diriez-vous de numpy? Cela fonctionne également avec des chaînes:

import numpy as np

np.array([[a,b] for a,b in zip([1,2,3],[2,3,4,5,6])]).ravel()

Résultat:

array([1, 2, 2, 3, 3, 4])
Nikolay Frick
la source
0

Une alternative de manière fonctionnelle et immuable (Python 3):

from itertools import zip_longest
from functools import reduce

reduce(lambda lst, zipped: [*lst, *zipped] if zipped[1] != None else [*lst, zipped[0]], zip_longest(list1, list2),[])
godot
la source
-1

Je ferais le simple:

chain.from_iterable( izip( list1, list2 ) )

Il fournira un itérateur sans créer de besoins de stockage supplémentaires.

Wheaties
la source
1
C'est vraiment simple, mais cela ne fonctionne qu'avec des listes de même longueur!
Jochen Ritzel le
Vous pouvez le résoudre avec chain.from_iterable(izip(list1, list2), list1[len(list2):])pour le problème particulier posé ici ... list1 est censé être le plus long.
Jochen Ritzel le
Ouais, mais je préfère trouver une solution qui fonctionne pour des conteneurs de longueur arbitraire ou céder aux solutions proposées ci-dessus.
Wheaties
-2

Je suis trop vieux pour ne pas comprendre les listes, alors:

import operator
list3 = reduce(operator.add, zip(list1, list2))
Tom Anderson
la source