Maneira pitônica de combinar duas listas de forma alternada?

91

Eu tenho duas listas, a primeira das quais certamente conterá exatamente um item a mais do que a segunda . Gostaria de saber a maneira mais Pythônica de criar uma nova lista cujos valores de índice ímpar vêm da primeira lista e cujos valores de índice ímpar vêm da segunda lista.

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

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

Isso funciona, mas não é bonito:

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

De que outra forma isso pode ser alcançado? Qual é a abordagem mais pitônica?

Davidchambers
fonte
2
possível duplicata de Alternando entre iteradores em Python
Felix Kling
Não é uma duplicata! A resposta aceita no artigo com link acima produz uma lista de tuplas, não uma única lista mesclada.
Paul Sasik,
@Paul: Sim, a resposta aceita não dá a solução completa. Leia os comentários e as outras respostas. A questão é basicamente a mesma e as outras soluções podem ser aplicadas aqui.
Felix Kling,
3
@Felix: Discordo respeitosamente. É verdade, as perguntas estão na mesma vizinhança, mas não são realmente duplicatas. Como prova vaga, dê uma olhada nas respostas potenciais aqui e compare com a outra pergunta.
Paul Sasik,
Confira estes: stackoverflow.com/questions/7529376/…
wordsforthewise

Respostas:

119

Esta é uma maneira de fazer isso fatiando:

>>> 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
fonte
3
Obrigado, Duncan. Não sabia que é possível especificar uma etapa ao fatiar. O que eu gosto nessa abordagem é a sua naturalidade. 1. Faça uma lista do comprimento correto. 2. Preenchido os índices pares com o conteúdo da lista1. 3. Preencha os índices ímpares com o conteúdo de list2. O fato de as listas terem comprimentos diferentes não é um problema neste caso!
davidchambers,
2
Acho que só funciona quando len (lista1) - len (lista2) é 0 ou 1.
xan
1
Se as listas tiverem comprimentos apropriados, então funciona; se não, a pergunta original não especifica a resposta esperada. Ele pode ser facilmente modificado para lidar com a maioria das situações razoáveis: por exemplo, se você quiser que elementos extras sejam ignorados, basta cortar a lista mais longa antes de começar; se você quiser que os elementos extras sejam intercalados com Nenhum, certifique-se de que o resultado seja inicializado com mais alguns Nenhum; se você quiser elementos extras apenas adicionados no final, faça como para ignorá-los e, em seguida, acrescente-os.
Duncan,
1
Eu também não estava claro. O que eu estava tentando enfatizar é que a solução de Duncan, ao contrário de muitas das listadas, não é complicada pelo fato de que as listas são de tamanho desigual. Claro, é aplicável apenas em uma gama limitada de situações, mas eu prefiro uma solução realmente elegante que funcione neste caso a uma solução menos elegante que funcione para quaisquer duas listas.
davidchambers,
1
Você pode usar (2 * len (lista1) -1) em vez de (len (lista1) + len (lista2)), também prefiro [0 :: 2] em vez de [:: 2].
Lord British
51

Há uma receita para isso na itertoolsdocumentação :

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

EDITAR:

Para a versão de python superior a 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
fonte
Acho isso mais complicado do que precisa ser. Há uma opção melhor abaixo usando zip_longest.
Dubslow
@Dubslow Para este caso específico, sim, isso provavelmente é um exagero (como mencionei em um comentário em outro lugar), a menos que você já tenha acesso a ele. Pode ter algumas vantagens em outras situações. Esta receita certamente não foi projetada para este problema, apenas acontece para resolvê-lo.
David Z de
1
fyi, você deve usar a receita na itertools documentação porque .next()não funciona mais.
john w.
1
@johnw. um tem que usar __next__. Não está escrito na documentação, então propus uma edição para a resposta.
Marine Galantin
@Marine Eu preferia que você tivesse acabado de alterar o exemplo de código existente, mas posso consertar isso sozinho. Obrigado por contribuir!
David Z
31

Isso deve fazer o que você quiser:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']
Mark Byers
fonte
Gostei muito da sua resposta inicial. Embora não respondesse perfeitamente à questão, era uma maneira elegante de mesclar duas listas do mesmo tamanho. Eu sugiro mantê-lo, junto com a advertência de comprimento, em sua resposta atual.
Paul Sasik,
1
Se, em vez disso, a lista1 fosse ['f', 'o', 'o', 'd'], seu item final ('d') não apareceria na lista resultante (o que é totalmente correto, dadas as especificidades da questão). Esta é uma solução elegante!
davidchambers
1
@Mark sim (eu
votei positivamente
4
+1 para resolver o problema declarado e simplesmente também :-) Achei que algo assim seria possível. Honestamente, acho que a roundrobinfunção é um pouco exagerada para essa situação.
David Z,
1
Para trabalhar com listas de qualquer tamanho, você pode simplesmente anexar o que resta nos iteradores ao resultado:list(itertools.chain(map(next, itertools.cycle(iters)), *iters))
panda-34
30
import itertools
print [x for x in itertools.chain.from_iterable(itertools.izip_longest(list1,list2)) if x]

Acho que essa é a maneira mais pitônica de fazer isso.

user942640
fonte
3
Por que essa não é a resposta aceita? Este é o mais curto e mais pitônico e funciona com diferentes comprimentos de lista!
Jairo Vadillo
5
o nome do método é zip_longest e não izip_longest
Jairo Vadillo
1
O problema com isso é que o valor de preenchimento padrão de zip_longest pode sobrescrever Nones que deveriam * estar na lista. Vou editar em uma versão ajustada para corrigir isso
Dubslow
Nota: Isso causará problemas se as listas contiverem elementos com valor False, ou mesmo coisas que só serão avaliadas como Falsepela ifexpressão-, como por exemplo a 0ou uma lista vazia. Isto pode ser (parcialmente) evitado pelo seguinte: [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]. Claro, isso ainda não funcionará se as listas contiverem Noneelementos que precisam ser preservados. Nesse caso, você precisa mudar o fillvalueargumento de zip_longest, como Dubslow já sugeriu.
der_herr_g
Noneo problema parece ter desaparecido, pelo menos desde o Python 3.7.6 (não sei para versões anteriores). Se alt_chainfor definido como def alt_chain(*iters, fillvalue=None): return chain.from_iterable(zip_longest(*iters, fillvalue=fillvalue)), então list(alt_chain([0, False, 1, set(), 3, 4], [0, None, 1, {}], fillvalue=99))retorna corretamente [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99].
paime
18

Sem ferramentas e assumindo que l1 é 1 item maior do que l2:

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

Usando itertools e assumindo que as listas não contêm Nenhum:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')
Zart
fonte
Esta é minha resposta favorita. É tão conciso.
mbomb007
@ anishtain4 zip pega pares de elementos como tuplas de listas [(l1[0], l2[0]), (l1[1], l2[1]), ...],. sumconcatena tuplas: (l1[0], l2[0]) + (l1[1], l2[1]) + ...resultando em listas intercaladas. O resto da linha é apenas preenchimento de l1 com elemento extra para zip trabalhar e cortar até -1 para se livrar desse preenchimento.
Zart
izip_longest (zip_longest desde python 3) não precisa de + [0] preenchimento, ele preenche implicitamente nenhum quando os comprimentos das listas não correspondem, enquanto filter(None, ...(poderia usar em boolvez disso, ou None.__ne__) remove valores falsos, incluindo 0, nenhum e strings vazias, então a segunda expressão não é estritamente equivalente à primeira.
Zart
A questão é como você fez sumisso? Qual é o papel do segundo argumento aí? Nas documentações, o segundo argumento é start.
anishtain4
O valor padrão de início é 0, e você não pode fazer 0+ (alguns, tupla), portanto, início é alterado para tupla vazia.
Zart de
13

Sei que as perguntas são sobre duas listas, uma com um item a mais do que a outra, mas imaginei que colocaria isso para outras pessoas que possam encontrar essa pergunta.

Aqui está a solução de Duncan adaptada para trabalhar com duas listas de tamanhos diferentes.

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

Isso resulta em:

['f', 'hello', 'o', 'world', 'o', 'b', 'a', 'r'] 
mhost
fonte
8

Se ambas as listas tiverem comprimento igual, você pode fazer:

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

Como a primeira lista tem mais um elemento, você pode adicioná-lo post hoc:

[x for y in zip(list1, list2) for x in y] + [list1[-1]]
Alguém
fonte
3
^ Esta deveria ser a resposta, python se tornou mais python nos últimos 10 anos
Tian
5

Aqui está um liner que faz isso:

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

Jay
fonte
2
Isso funciona corretamente, mas me parece deselegante, uma vez que está fazendo muito para alcançar algo tão simples. Não estou dizendo que essa abordagem seja ineficiente, simplesmente que não é particularmente fácil de ler.
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
killown
fonte
Tenha muito cuidado ao usar argumentos padrão mutáveis. Isso só retornará a resposta correta na primeira vez que for chamada, pois será reutilizada em todas as chamadas subsequentes. Seria melhor escrever como lst = None ... se lst for None: lst = [], embora não veja uma razão convincente para optar por essa abordagem em vez de outras listadas aqui.
davidchambers,
lst é definido dentro da função, portanto é uma variável local. O problema potencial é que list1 e list2 are serão reutilizados toda vez que você usar a função, mesmo se você chamar a função com listas diferentes. Consulte docs.python.org/tutorial/…
blokeley de
1
@blokeley: errado, seria reutilizado se fosse combinado (list1 = [...], list2 = [...])
killown 10/09/10
Quando esta solução foi postada pela primeira vez def combine(list1, list2, lst=[]):, sua primeira linha lida , daí meu comentário. No momento em que enviei esse comentário, entretanto, killown havia feito a mudança necessária.
davidchambers
2

Este é baseado na contribuição de Carlos Valiente acima com uma opção para alternar grupos de vários itens e certificar-se de que todos os itens estão presentes na saída:

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

Isso entrelaçará as listas A e B por grupos de 3 valores cada:

['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
fonte
2

Pode ser um pouco tarde para comprar mais uma linha de python. Isso funciona quando as duas listas têm tamanhos iguais ou desiguais. Uma coisa que não vale nada é que modificará a e b. Se for um problema, você precisará usar outras soluções.

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
fonte
1

Minha vez:

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
fonte
1

Aqui está uma linha usando compreensões de lista, sem outras bibliotecas:

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

Aqui está outra abordagem, se você permitir a alteração de sua lista1 inicial por efeito colateral:

[list1.insert((i+1)*2-1, list2[i]) for i in range(len(list2))]
Chernevik
fonte
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
fonte
0

Pára no mais curto:

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

Claro, resolver o método next / __ next__ pode ser mais rápido.

jwp
fonte
0

Isso é desagradável, mas funciona independentemente do tamanho das listas:

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

Vários one-liners inspirados por respostas a outra pergunta :

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]
palavras para o sábio
fonte
0

Que tal entorpecido? Também funciona com strings:

import numpy as np

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

Resultado:

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

Uma alternativa de forma funcional e imutável (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
fonte
-1

Eu faria o simples:

chain.from_iterable( izip( list1, list2 ) )

Ele virá com um iterador sem criar nenhuma necessidade de armazenamento adicional.

trigo
fonte
1
É muito simples, mas só funciona com listas do mesmo tamanho!
Jochen Ritzel,
Você pode corrigi-lo com chain.from_iterable(izip(list1, list2), list1[len(list2):])para o problema específico perguntado aqui ... a lista1 deve ser a mais longa.
Jochen Ritzel,
Sim, mas eu prefiro encontrar uma solução que funcione para contêineres de comprimento arbitrário ou render para as soluções propostas acima.
trigo
-2

Estou muito velho para entender listas, então:

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