Pares de una sola lista

98

A menudo, he encontrado la necesidad de procesar una lista por pares. Me preguntaba cuál sería la forma pitónica y eficiente de hacerlo, y encontré esto en Google:

pairs = zip(t[::2], t[1::2])

Pensé que era lo suficientemente pitónico, pero después de una discusión reciente que involucró modismos versus eficiencia , decidí hacer algunas pruebas:

import time
from itertools import islice, izip

def pairs_1(t):
    return zip(t[::2], t[1::2]) 

def pairs_2(t):
    return izip(t[::2], t[1::2]) 

def pairs_3(t):
    return izip(islice(t,None,None,2), islice(t,1,None,2))

A = range(10000)
B = xrange(len(A))

def pairs_4(t):
    # ignore value of t!
    t = B
    return izip(islice(t,None,None,2), islice(t,1,None,2))

for f in pairs_1, pairs_2, pairs_3, pairs_4:
    # time the pairing
    s = time.time()
    for i in range(1000):
        p = f(A)
    t1 = time.time() - s

    # time using the pairs
    s = time.time()
    for i in range(1000):
        p = f(A)
        for a, b in p:
            pass
    t2 = time.time() - s
    print t1, t2, t2-t1

Estos fueron los resultados en mi computadora:

1.48668909073 2.63187503815 1.14518594742
0.105381965637 1.35109519958 1.24571323395
0.00257992744446 1.46182489395 1.45924496651
0.00251388549805 1.70076990128 1.69825601578

Si los estoy interpretando correctamente, eso debería significar que la implementación de listas, indexación de listas y división de listas en Python es muy eficiente. Es un resultado reconfortante e inesperado.

¿Existe otra forma "mejor" de recorrer una lista por parejas?

Tenga en cuenta que si la lista tiene un número impar de elementos, el último no estará en ninguno de los pares.

¿Cuál sería la forma correcta de garantizar que se incluyan todos los elementos?

Agregué estas dos sugerencias de las respuestas a las pruebas:

def pairwise(t):
    it = iter(t)
    return izip(it, it)

def chunkwise(t, size=2):
    it = iter(t)
    return izip(*[it]*size)

Estos son los resultados:

0.00159502029419 1.25745987892 1.25586485863
0.00222492218018 1.23795199394 1.23572707176

Resultados hasta ahora

Más pitónico y muy eficiente:

pairs = izip(t[::2], t[1::2])

Más eficiente y muy pitónico:

pairs = izip(*[iter(t)]*2)

Me tomó un momento asimilar que la primera respuesta usa dos iteradores mientras que la segunda usa uno solo.

Para tratar con secuencias con un número impar de elementos, la sugerencia ha sido aumentar la secuencia original agregando un elemento ( None) que se empareja con el último elemento anterior, algo con lo que se puede lograr itertools.izip_longest().

Finalmente

Tenga en cuenta que, en Python 3.x, se zip()comporta como itertools.izip()y itertools.izip() se ha ido.

Apalala
fuente
RE: el "camino correcto" - ¡no hay un camino "correcto"! Depende del caso de uso.
Andrew Jaffe
@Andrew Jaffe Di el criterio de "mejor" en este caso: eficiente y pitónico.
Apalala
@Apalala: Quiero decir que el resultado de tener un número impar depende del uso. Por ejemplo: podría simplemente dejar fuera el último elemento, o agregar un elemento ficticio conocido específico, o duplicar el último
Andrew Jaffe
2
@Apalala: porque estás usando palabrería en lugar del timeitmódulo.
SilentGhost

Respuestas:

52

Mi forma favorita de hacerlo:

from itertools import izip

def pairwise(t):
    it = iter(t)
    return izip(it,it)

# for "pairs" of any length
def chunkwise(t, size=2):
    it = iter(t)
    return izip(*[it]*size)

Cuando desee emparejar todos los elementos, obviamente es posible que necesite un valor de relleno:

from itertools import izip_longest
def blockwise(t, size=2, fillvalue=None):
    it = iter(t)
    return izip_longest(*[it]*size, fillvalue=fillvalue)
Jochen Ritzel
fuente
A la primera función (por pares) parece que le falta la clonación y el avance del segundo iterador. Vea la itertoolssección de recetas.
Apalala
@Apalala: zip avanza el mismo iterador dos veces.
Jochen Ritzel
Por supuesto, tienes razón, y por pares es el más eficiente hasta ahora, no sé por qué.
Apalala
1
Me encanta esta solución: es perezosa y explota el estado de los iteradores con gran efecto. Incluso podría hacerlo de una sola línea, aunque quizás a expensas de la legibilidad:izip(*[iter(t)]*size)
Channing Moore
para su segunda solución, ¿no querría evitar la creación de una lista si busca rendimiento?
máximo
41

Yo diría que su solución inicial pairs = zip(t[::2], t[1::2])es la mejor porque es más fácil de leer (y en Python 3, zipdevuelve automáticamente un iterador en lugar de una lista).

Para asegurarse de que todos los elementos estén incluidos, simplemente puede ampliar la lista en None.

Entonces, si la lista tiene un número impar de elementos, el último par será (item, None).

>>> t = [1,2,3,4,5]
>>> t.append(None)
>>> zip(t[::2], t[1::2])
[(1, 2), (3, 4), (5, None)]
>>> t = [1,2,3,4,5,6]
>>> t.append(None)
>>> zip(t[::2], t[1::2])
[(1, 2), (3, 4), (5, 6)]
Tim Pietzcker
fuente
6

Empiezo con un pequeño descargo de responsabilidad: no use el código a continuación. No es Pythonic en absoluto, escribí solo por diversión. Es similar a la pairwisefunción @ THC4k pero usa itery lambdacierra. No usa itertoolsmódulo y no es compatible fillvalue. Lo puse aquí porque alguien podría encontrarlo interesante:

pairwise = lambda t: iter((lambda f: lambda: (f(), f()))(iter(t).next), None)
Tomasz Elendt
fuente
4

En lo que respecta a la mayoría de Pythonic, diría que las recetas proporcionadas en los documentos fuente de Python (algunas de las cuales se parecen mucho a las respuestas que @JochenRitzel proporcionó) son probablemente su mejor opción;)

def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
    args = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *args)

En Python moderno, solo tiene que usarlo de zip_longest(*args, fillvalue=fillvalue) acuerdo con la página del documento correspondiente .

Palmadita
fuente
2

¿Existe otra forma "mejor" de recorrer una lista por parejas?

No puedo decirlo con certeza, pero lo dudo: cualquier otro recorrido incluiría más código Python que debe interpretarse. Las funciones integradas como zip () están escritas en C, que es mucho más rápido.

¿Cuál sería la forma correcta de garantizar que se incluyan todos los elementos?

Verifique la longitud de la lista y si es impar ( len(list) & 1 == 1), copie la lista y agregue un elemento.

Aaron Digulla
fuente
2
>>> my_list = [1,2,3,4,5,6,7,8,9,10]
>>> my_pairs = list()
>>> while(my_list):
...     a = my_list.pop(0); b = my_list.pop(0)
...     my_pairs.append((a,b))
... 
>>> print(my_pairs)
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
Diarmuid O'Briain
fuente
IndexError: emergente de la lista vacía
HQuser
@HQuser Seguro, obtendrá ese error si tiene un número impar de elementos en la lista. Debe saber con certeza que tiene pares o verificar esta condición de error.
WaterMolecule
0

Solo hazlo:

>>> l = [1, 2, 3, 4, 5, 6]
>>> [(x,y) for x,y in zip(l[:-1], l[1:])]
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
Israel Gonçaves de Oliveira
fuente
Su código es equivalente al más simple list(zip(l, l[1:]))y no divide la lista en pares.
Apalala
0

Aquí hay un ejemplo de cómo crear pares / piernas usando un generador. Los generadores están libres de límites de pila

def pairwise(data):
    zip(data[::2], data[1::2])

Ejemplo:

print(list(pairwise(range(10))))

Salida:

[(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]
Vlad Bezden
fuente
¿Comparación de tiempo de ejecución?
Alan
La lista no está dividida en pares, ya que la mayoría de los números de la lista original aparecen en dos tuplas. La salida esperada es[(0, 1), (2, 3), (4, 5)....
Apalala
@Apalala gracias por señalar.
Arreglé
zip()ya devuelve un generador en Python 3.x, @VladBezden
Apalala
-1

En caso de que alguien necesite el algoritmo de respuesta, aquí está:

>>> def getPairs(list):
...     out = []
...     for i in range(len(list)-1):
...         a = list.pop(0)
...         for j in a:
...             out.append([a, j])
...     return b
>>> 
>>> k = [1, 2, 3, 4]
>>> l = getPairs(k)
>>> l
[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

Pero tenga en cuenta que su lista original también se reducirá a su último elemento, porque utilizó popen él.

>>> k
[4]
frainmaster
fuente