¿Manera pitónica de combinar dos listas de forma alterna?

88

Tengo dos listas, la primera de las cuales está garantizada para contener exactamente un artículo más que la segunda . Me gustaría saber la forma más Pythonic de crear una nueva lista cuyos valores de índice par provienen de la primera lista y cuyos valores de índice impar provienen de la segunda lista.

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

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

Esto funciona, pero no es bonito:

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

¿De qué otra manera se puede lograr esto? ¿Cuál es el enfoque más Pythonic?

Davidchambers
fuente
1
posible duplicado de alternancia entre iteradores en Python
Felix Kling
¡No es un duplicado! La respuesta aceptada en el artículo vinculado anteriormente produce una lista de tuplas, no una única lista combinada.
Paul Sasik
@Paul: Sí, la respuesta aceptada no da la solución completa. Lea los comentarios y las otras respuestas. La pregunta es básicamente la misma y las otras soluciones se pueden aplicar aquí.
Felix Kling
3
@Felix: Respetuosamente no estoy de acuerdo. Es cierto, las preguntas están en el mismo barrio, pero en realidad no se duplican. Como prueba vaga, eche un vistazo a las posibles respuestas aquí y compárelas con la otra pregunta.
Paul Sasik
Echa un vistazo a estos: stackoverflow.com/questions/7529376/…
wordsforthewise

Respuestas:

112

Aquí hay una forma de hacerlo cortando:

>>> 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
fuente
3
Gracias, Duncan. No me di cuenta de que es posible especificar un paso al cortar. Lo que me gusta de este enfoque es la naturalidad con la que se lee. 1. Haga una lista de la longitud correcta. 2. Rellenó los índices pares con el contenido de list1. 3. Complete los índices impares con el contenido de list2. ¡El hecho de que las listas sean de diferentes longitudes no es un problema en este caso!
Davidchambers
2
Creo que solo funciona cuando len (list1) - len (list2) es 0 o 1.
xan
1
Si las listas tienen la longitud adecuada, entonces funciona, si no, la pregunta original no especifica qué respuesta se espera. Puede modificarse fácilmente para manejar situaciones más razonables: por ejemplo, si desea que se ignoren elementos adicionales, simplemente corte la lista más larga antes de comenzar; si desea que los elementos adicionales se intercalen con Ninguno, asegúrese de que el resultado se inicialice con algunos Ninguno más; si desea que se agreguen elementos adicionales al final, haga lo mismo para ignorarlos y luego añádalos.
Duncan
1
Yo tampoco estaba claro. El punto que estaba tratando de hacer es que la solución de Duncan, a diferencia de muchas de las enumeradas, no se complica por el hecho de que las listas tienen una longitud desigual. Claro, es aplicable solo en una gama limitada de situaciones, pero prefiero una solución realmente elegante que funcione en este caso a una solución menos elegante que funcione para dos listas cualesquiera.
Davidchambers
1
Puede usar (2 * len (list1) -1) en lugar de (len (list1) + len (list2)), también prefiero [0 :: 2] en lugar de [:: 2].
Lord British
50

Hay una receta para esto en la itertoolsdocumentación :

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 la versión de Python mayor que 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
fuente
Encuentro esta forma más complicada de lo que debería ser. Hay una mejor opción a continuación usando zip_longest.
Dubslow
@Dubslow Para este caso particular, sí, esto probablemente sea excesivo (como mencioné en un comentario en otro lugar), a menos que ya tenga acceso a él. Sin embargo, podría tener algunas ventajas en otras situaciones. Esta receta ciertamente no fue diseñada para este problema, simplemente lo resuelve.
David Z
1
Para tu información, deberías usar la receta en la itertools documentación porque .next()ya no funciona.
john w.
1
@johnw. uno tiene que usar __next__. No está escrito en la documentación, así que propuse una edición a la respuesta.
Marine Galantin
@Marine Preferiría que acabaras de cambiar la muestra de código existente, pero puedo solucionarlo yo mismo. ¡Gracias por contribuir!
David Z
31

Esto debería hacer lo que quieras:

>>> iters = [iter(list1), iter(list2)]
>>> print list(it.next() for it in itertools.cycle(iters))
['f', 'hello', 'o', 'world', 'o']
Mark Byers
fuente
Realmente me gustó tu respuesta inicial. Aunque no abordó perfectamente la pregunta, fue una forma elegante de fusionar dos listas de la misma longitud. Sugiero conservarlo, junto con la advertencia de longitud, en su respuesta actual.
Paul Sasik
1
Si list1 fuera ['f', 'o', 'o', 'd'], su elemento final ('d') no aparecería en la lista resultante (lo cual está totalmente bien dados los detalles de la pregunta). ¡Esta es una solución elegante!
davidchambers
1
@Mark sí (lo voté a favor), solo señalando las diferencias (y las limitaciones si otras personas quieren un comportamiento diferente)
cobbal
4
+1 por resolver el problema indicado, y simplemente por hacerlo también :-) Pensé que algo como esto sería posible. Honestamente, creo que la roundrobinfunción es excesiva para esta situación.
David Z
1
Para trabajar con listas de cualquier tamaño, simplemente puede agregar lo que queda en los iteradores al resultado: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]

Creo que esta es la forma más pitónica de hacerlo.

usuario942640
fuente
3
¿Por qué esta no es la respuesta aceptada? ¡Este es el más corto y pitónico y funciona con diferentes longitudes de lista!
Jairo Vadillo
5
el nombre del método es zip_longest no izip_longest
Jairo Vadillo
1
El problema con esto es que el valor de relleno predeterminado de zip_longest podría sobrescribir Nonelos mensajes de correo electrónico * que se supone que están en la lista.
Editaré
Nota: Esto causará problemas si las listas contienen elementos con valor False, o incluso cosas que solo serán evaluaron como Falsepor el if-expresión, como por ejemplo, una 0o una lista vacía. Esto puede ser (parcialmente) evitarse por la siguiente: [x for x in itertools.chain.from_iterable(itertools.zip_longest(list1, list2)) if x is not None]. Por supuesto, esto aún no funcionará si las listas contienen Noneelementos que deben conservarse. En este caso, debe cambiar el fillvalueargumento de zip_longest, como Dubslow ya sugirió.
der_herr_g
NoneEl problema parece haber desaparecido, al menos desde Python 3.7.6 (no lo sé para versiones anteriores). Si alt_chainse define como 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))devuelve correctamente [0, 0, False, None, 1, 1, set(), {}, 3, 99, 4, 99].
paime
17

Sin itertools y asumiendo que l1 es 1 elemento más largo que l2:

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

Usando itertools y asumiendo que las listas no contienen None:

>>> filter(None, sum(itertools.izip_longest(l1, l2), ()))
('f', 'hello', 'o', 'world', 'o')
Zart
fuente
Esta es mi respuesta favorita. Es tan conciso.
mbomb007
@ anishtain4 zip toma pares de elementos como tuplas de listas, [(l1[0], l2[0]), (l1[1], l2[1]), ...]. sumconcatena tuplas juntas: (l1[0], l2[0]) + (l1[1], l2[1]) + ...resultando en listas intercaladas. El resto del diagrama unifilar es solo el relleno de l1 con un elemento adicional para que funcione la cremallera y corte hasta -1 para deshacerse de ese relleno.
Zart
izip_longest (zip_longest desde python 3) no necesita relleno + [0], implícitamente llena None cuando las longitudes de las listas no coinciden, mientras que filter(None, ...(podría usar boolen su lugar, o None.__ne__) elimina los valores falsos, incluidos 0, None y cadenas vacías, por lo que la segunda expresión no es estrictamente equivalente a la primera.
Zart
La pregunta es ¿cómo hiciste sumeso? ¿Cuál es el papel del segundo argumento allí? En las documentaciones, el segundo argumento es start.
anishtain4
El valor predeterminado de inicio es 0 y no puede hacer 0+ (algunos, tupla), por lo que el inicio se cambia a tupla vacía.
Zart
13

Sé que las preguntas se refieren a dos listas en las que una tiene un elemento más que la otra, pero pensé que pondría esto para otras personas que puedan encontrar esta pregunta.

Aquí está la solución de Duncan adaptada para trabajar con dos listas de diferentes tamaños.

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

Esto produce:

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

Si ambas listas tienen la misma longitud, puede hacer:

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

Como la primera lista tiene un elemento más, puede agregarlo post hoc:

[x for y in zip(list1, list2) for x in y] + [list1[-1]]
Alguien
fuente
2
^ Esta debería ser la respuesta, Python se ha vuelto más pitónico en los últimos 10 años
Tian
5

Aquí hay un trazador de líneas que lo hace:

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

Arrendajo
fuente
2
Esto funciona correctamente, pero me parece poco elegante ya que está haciendo mucho para lograr algo tan simple. No estoy diciendo que este enfoque sea ineficaz, simplemente que no es particularmente fácil de leer.
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
horno
fuente
Tenga mucho cuidado al utilizar argumentos predeterminados mutables. Esto solo devolverá la respuesta correcta la primera vez que se llame, ya que lst se reutilizará para cada llamada a partir de entonces. Esto estaría mejor escrito como lst = None ... si lst es None: lst = [], aunque no veo una razón de peso para optar por este enfoque sobre otros enumerados aquí.
Davidchambers
lst se define dentro de la función, por lo que es una variable local. El problema potencial es que list1 y list2 se reutilizarán cada vez que use la función, incluso si llama a la función con listas diferentes. Ver docs.python.org/tutorial/…
blokeley
1
@blokeley: incorrecto, se reutilizaría si fuera combinado (lista1 = [...], lista2 = [...])
killown
Cuando esta solución se publicó por primera vez def combine(list1, list2, lst=[]):, se leyó la primera línea , de ahí mi comentario. Sin embargo, cuando envié ese comentario, Killown había realizado el cambio necesario.
davidchambers
2

Este se basa en la contribución de Carlos Valiente anterior con una opción para alternar grupos de varios elementos y asegurarse de que todos los elementos estén presentes en la salida:

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

Esto entrelazará las listas A y B por grupos de 3 valores cada uno:

['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]
cantado
fuente
2

Puede que sea un poco tarde para comprar otra línea de Python. Esto funciona cuando las dos listas tienen un tamaño igual o desigual. Una cosa que no vale nada es que modificará ay b. Si es un problema, debe utilizar otras soluciones.

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

Mi toma:

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

Aquí hay una línea que usa listas por comprensión, sin otras bibliotecas:

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

Aquí hay otro enfoque, si permite la alteración de su list1 inicial por efecto secundario:

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

Se detiene en el más corto:

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 el método next / __ next__ puede ser más rápido.

jwp
fuente
0

Esto es desagradable pero funciona sin importar el tamaño de las listas:

list3 = [element for element in list(itertools.chain.from_iterable([val for val in itertools.izip_longest(list1, list2)])) if element != None]
caballero araña
fuente
0

Varias frases ingeniosas inspiradas en las respuestas a otra pregunta :

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]
palabras por lo demás
fuente
0

¿Qué tal numpy? También funciona con cadenas:

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
fuente
0

Una alternativa de forma funcional e inmutable (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
fuente
-1

Haría lo simple:

chain.from_iterable( izip( list1, list2 ) )

Aparecerá con un iterador sin crear ninguna necesidad de almacenamiento adicional.

Wheaties
fuente
1
Es realmente simple, ¡pero solo funciona con listas de la misma longitud!
Jochen Ritzel
Puede solucionarlo con chain.from_iterable(izip(list1, list2), list1[len(list2):])el problema particular que se pregunta aquí ... se supone que list1 es el más largo.
Jochen Ritzel
Sí, pero prefiero encontrar una solución que funcione para contenedores de longitud arbitraria o ceder a las soluciones propuestas anteriormente.
Wheaties
-2

Soy demasiado mayor para estar deprimido con listas por comprensión, así que:

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