Modo pitonico per combinare due elenchi in modo alternato?


88

Ho due elenchi, il primo dei quali è garantito per contenere esattamente un elemento in più rispetto al secondo . Vorrei conoscere il modo più pitonico per creare una nuova lista i cui valori di indice pari provengono dalla prima lista e i cui valori di indice dispari provengono dalla seconda lista.

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

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

Funziona, ma non è carino:

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

In quale altro modo può essere ottenuto? Qual è l'approccio più pitonico?



Non un duplicato! La risposta accettata nell'articolo sopra collegato produce un elenco di tuple, non un unico elenco unito.
Paul Sasik

@Paul: Sì, la risposta accettata non fornisce la soluzione completa. Leggi i commenti e le altre risposte. La domanda è fondamentalmente la stessa e le altre soluzioni possono essere applicate qui.
Felix Kling,

3
@ Felix: rispettosamente non sono d'accordo. È vero, le domande sono nello stesso quartiere ma non sono realmente duplicati. Come vaga prova, dai un'occhiata alle potenziali risposte qui e confrontale con l'altra domanda.
Paul Sasik

Risposte:


112

Ecco un modo per farlo affettando:

>>> 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']

3
Grazie, Duncan. Non mi rendevo conto che è possibile specificare un passaggio durante l'affettatura. Quello che mi piace di questo approccio è la naturalezza con cui legge. 1. Fare un elenco della lunghezza corretta. 2. Popolato gli indici pari con il contenuto di list1. 3. Popolare gli indici dispari con il contenuto di list2. Il fatto che gli elenchi abbiano lunghezze diverse non è un problema in questo caso!
davidchambers

2
Penso che funzioni solo quando len (list1) - len (list2) è 0 o 1.
xan

1
Se gli elenchi sono di lunghezze appropriate, allora funziona, altrimenti la domanda originale non specifica quale risposta è prevista. Può essere facilmente modificato per gestire le situazioni più ragionevoli: ad esempio, se si desidera che gli elementi aggiuntivi vengano ignorati, basta abbattere l'elenco più lungo prima di iniziare; se vuoi che gli elementi extra siano intercalati con Nessuno, assicurati che il risultato sia inizializzato con altri Nessuno; se vuoi aggiungere elementi extra alla fine, fai come per ignorarli e poi aggiungili.
Duncan

1
Anch'io non ero chiaro. Il punto che stavo cercando di sottolineare è che la soluzione di Duncan, a differenza di molte di quelle elencate, non è complicata dal fatto che le liste sono di lunghezza diversa. Certo, è applicabile solo in una gamma limitata di situazioni, ma preferirei una soluzione davvero elegante che funzioni in questo caso a una soluzione meno elegante che funzioni per due elenchi qualsiasi.
davidchambers

1
Puoi usare (2 * len (list1) -1) invece di (len (list1) + len (list2)), inoltre preferisco [0 :: 2] invece di [:: 2].
Lord British

50

C'è una ricetta per questo nella itertoolsdocumentazione :

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))

MODIFICARE:

Per la versione di Python maggiore di 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))

Trovo in questo modo più complicato di quanto dovrebbe essere. C'è un'opzione migliore di seguito usando zip_longest.
Dubslow

@Dubslow Per questo caso particolare, sì, probabilmente è eccessivo (come ho detto in un commento altrove), a meno che tu non abbia già accesso ad esso. Tuttavia, potrebbe avere alcuni vantaggi in altre situazioni. Questa ricetta certamente non è stata pensata per questo problema, capita solo di risolverlo.
David Z

1
fyi dovresti usare la ricetta nella itertools documentazione perché .next()non funziona più.
john w.

1
@johnw. uno deve usare __next__. Non è scritto nella documentazione, quindi ho proposto una modifica alla risposta.
Marine Galantin il

@ Marine Preferirei che tu avessi appena modificato l'esempio di codice esistente, ma posso risolverlo da solo. Grazie per aver contribuito!
David Z

31

Questo dovrebbe fare quello che vuoi:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']

Mi è davvero piaciuta la tua risposta iniziale. Sebbene non rispondesse perfettamente alla domanda, era un modo elegante per unire due elenchi della stessa lunghezza. Suggerisco di mantenerlo, insieme all'avvertenza sulla lunghezza, nella tua risposta attuale.
Paul Sasik

1
Se list1 fosse invece ['f', 'o', 'o', 'd'], il suo elemento finale ('d') non apparirebbe nell'elenco risultante (il che è assolutamente corretto date le specifiche della domanda). Questa è una soluzione elegante!
davidchambers

1
@Mark yep (ho votato positivamente), sottolineando solo le differenze (e i limiti se altre persone vogliono un comportamento diverso)
cobbal

4
+1 per aver risolto il problema indicato e anche per averlo fatto semplicemente :-) Ho pensato che sarebbe stato possibile qualcosa del genere. Onestamente penso che la roundrobinfunzione sia un po 'eccessiva per questa situazione.
David Z

1
Per lavorare con elenchi di qualsiasi dimensione puoi semplicemente aggiungere al risultato ciò che resta negli iteratori: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]

Penso che questo sia il modo più pitonico per farlo.


3
Perché questa non è la risposta accettata? Questo è il più corto e il più pitonico e funziona con liste di lunghezze diverse!
Jairo Vadillo

5
il nome del metodo è zip_longest non izip_longest
Jairo Vadillo

1
Il problema con questo è che il valore di riempimento predefinito da zip_longest potrebbe sovrascrivere Nonei messaggi di posta * che dovrebbero essere nella lista.
Modificherò

Nota: Questo può causare problemi se gli elenchi contengono elementi con valore False, o cose anche che verranno esclusivamente valutati come Falsedalla if-expression, come ad esempio un 0o un elenco vuoto. Questo può essere (parzialmente) evitata la seguente: [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]. Ovviamente, questo non funzionerà ancora se gli elenchi contengono Noneelementi che devono essere preservati. In questo caso, è necessario modificare l' fillvalueargomento di zip_longest, come già suggerito da Dubslow.
der_herr_g

Noneil problema sembra essere scomparso, almeno da Python 3.7.6 (non lo so per le versioni precedenti). Se alt_chainè definito come def alt_chain(*iters, fillvalue=None): return chain.from_iterable(zip_longest(*iters, fillvalue=fillvalue)), list(alt_chain([0, False, 1, set(), 3, 4], [0, None, 1, {}], fillvalue=99))restituisce correttamente [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99].
Paime

17

Senza itertools e assumendo che l1 sia 1 elemento più lungo di l2:

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

Usando itertools e assumendo che gli elenchi non contengano Nessuno:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')

Questa è la mia risposta preferita. È così conciso.
mbomb007

@ anishtain4 zip richiederà coppie di elementi come tuple di liste, [(l1[0], l2[0]), (l1[1], l2[1]), ...]. sumconcatena le tuple insieme: (l1[0], l2[0]) + (l1[1], l2[1]) + ...risultando in elenchi interleaved. Il resto della riga è solo imbottitura di l1 con un elemento extra per far funzionare zip e tagliare fino a -1 per sbarazzarsi di quell'imbottitura.
Zart

izip_longest (zip_longest da python 3) non ha bisogno di + [0] riempimento, riempie implicitamente None quando le lunghezze degli elenchi non corrispondono, mentre filter(None, ...(potrebbe usare boolinvece, o None.__ne__) rimuove i valori falsi, inclusi 0, None e stringhe vuote, quindi la seconda espressione non è strettamente equivalente alla prima.
Zart

La domanda è: come hai fatto a sumfarlo? Qual è il ruolo del secondo argomento lì? Nella documentazione, il secondo argomento è start.
anishtain4

Il valore predefinito di start è 0 e non è possibile eseguire 0+ (alcuni, tuple), quindi start viene modificato in tupla vuota.
Zart

13

So che le domande fanno su due elenchi con uno che ha un elemento in più dell'altro, ma ho pensato di metterlo per altri che potrebbero trovare questa domanda.

Ecco la soluzione di Duncan adattata per funzionare con due elenchi di dimensioni diverse.

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

Questo produce:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 

6

Se entrambi gli elenchi hanno la stessa lunghezza, puoi fare:

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

Poiché il primo elenco ha un elemento in più, puoi aggiungerlo post hoc:

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

2
^ Questa dovrebbe essere la risposta, il pitone è diventato più pitonico negli ultimi 10 anni
Tian

5

Ecco una linea che lo fa:

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


2
Funziona correttamente ma mi sembra poco elegante poiché sta facendo così tanto per ottenere qualcosa di così semplice. Non sto dicendo che questo approccio sia inefficiente, semplicemente che non è particolarmente facile da leggere.
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

Fai molta attenzione quando usi argomenti predefiniti modificabili. Ciò restituirà la risposta corretta solo la prima volta che viene chiamato, poiché la prima verrà riutilizzata per ogni chiamata successiva. Sarebbe meglio scrivere come lst = None ... se lst è None: lst = [], anche se non vedo una ragione convincente per optare per questo approccio rispetto ad altri elencati qui.
davidchambers

lst è definito all'interno della funzione, quindi è una variabile locale. Il potenziale problema è che list1 e list2 verranno riutilizzate ogni volta che si utilizza la funzione, anche se si chiama la funzione con elenchi diversi. Vedi docs.python.org/tutorial/…
blokeley

1
@blokeley: sbagliato, verrebbe riutilizzato se fosse combinato (list1 = [...], list2 = [...])
killown

Quando questa soluzione è stata pubblicata per la prima volta def combine(list1, list2, lst=[]):, è stata letta la prima riga , da cui il mio commento. Quando ho presentato quel commento, però, Killown aveva apportato il cambiamento necessario.
davidchambers,

2

Questo si basa sul contributo di Carlos Valiente sopra con un'opzione per alternare gruppi di più elementi e assicurarsi che tutti gli elementi siano presenti nell'output:

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)]

Questo intreccia le liste A e B per gruppi di 3 valori ciascuno:

['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]

2

Potrebbe essere un po 'tardi, compra ancora un'altra battuta in pitone. Funziona quando i due elenchi hanno dimensioni uguali o diverse. Una cosa che non vale niente è che modificherà a e b. Se è un problema, devi utilizzare altre soluzioni.

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']

1

La mia opinione:

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)]

1

Ecco una riga che utilizza le comprensioni degli elenchi, senza altre librerie:

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

Ecco un altro approccio, se consenti l'alterazione della tua lista iniziale1 per effetto collaterale:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]

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']

0

Si ferma sul più breve:

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]))

Certo, la risoluzione del metodo next / __ next__ potrebbe essere più veloce.


0

Questo è brutto ma funziona indipendentemente dalle dimensioni degli elenchi:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]

0

Più battute ispirate dalle risposte a un'altra domanda :

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]

0

Che ne dici di numpy? Funziona anche con le stringhe:

import numpy as np

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

Risultato:

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

0

Un'alternativa in modo funzionale e immutabile (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),[])

-1

Farei il semplice:

chain.from_iterable( izip( list1, list2 ) )

Verrà fornito un iteratore senza creare esigenze di archiviazione aggiuntive.


1
È davvero semplice, ma funziona solo con elenchi della stessa lunghezza!
Jochen Ritzel

Puoi risolverlo con chain.from_iterable(izip(list1, list2), list1[len(list2):])per il problema particolare chiesto qui ... list1 dovrebbe essere quello più lungo.
Jochen Ritzel,

Sì, ma preferirei trovare una soluzione che funzioni per contenitori di lunghezza arbitraria o cedere alle soluzioni proposte sopra.
wheaties

-2

Sono troppo vecchio per essere giù con le comprensioni degli elenchi, quindi:

import operator
list3 = reduce(operator.add, zip(list1, list2))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.