Pares da lista única

98

Muitas vezes, descobri a necessidade de processar uma lista por pares. Eu estava me perguntando qual seria a maneira pítônica e eficiente de fazer isso, e encontrei isso no Google:

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

Achei que era pythônico o suficiente, mas depois de uma discussão recente envolvendo expressões idiomáticas versus eficiência , decidi fazer alguns testes:

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

Estes foram os resultados no meu computador:

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

Se estou interpretando-os corretamente, isso deve significar que a implementação de listas, indexação de lista e divisão de lista em Python é muito eficiente. É um resultado reconfortante e inesperado.

Existe outra maneira "melhor" de percorrer uma lista em pares?

Observe que se a lista tiver um número ímpar de elementos, o último não estará em nenhum dos pares.

Qual seria a maneira certa de garantir que todos os elementos sejam incluídos?

Eu adicionei estas duas sugestões das respostas aos testes:

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

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

Estes são os resultados:

0.00159502029419 1.25745987892 1.25586485863
0.00222492218018 1.23795199394 1.23572707176

Resultados até agora

Mais pitônico e muito eficiente:

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

Mais eficiente e muito pitônico:

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

Levei um momento para grocar que a primeira resposta usa dois iteradores enquanto a segunda usa um único.

Para lidar com sequências com um número ímpar de elementos, a sugestão foi aumentar a sequência original adicionando um elemento ( None) que é emparelhado com o último elemento anterior, algo que pode ser alcançado itertools.izip_longest().

Finalmente

Observe que, no Python 3.x, zip()se comporta como itertools.izip()e itertools.izip() desaparece.

Apalala
fonte
RE: o "jeito certo" - não existe um jeito "certo"! Depende do caso de uso.
Andrew Jaffe
@Andrew Jaffe Eu dei os critérios para "melhor" neste caso: eficiente e pythônico.
Apalala
@Apalala: Quero dizer que o resultado de ter um número ímpar depende do uso. Por exemplo: você pode simplesmente omitir o último elemento ou adicionar um elemento fictício conhecido específico ou duplicar o último
Andrew Jaffe
2
@Apalala: porque você está usando algum mumbo-jumbo em vez do timeitmódulo.
SilentGhost

Respostas:

52

Minha maneira favorita de fazer isso:

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)

Quando você deseja emparelhar todos os elementos, obviamente pode precisar de um fillvalue:

from itertools import izip_longest
def blockwise(t, size=2, fillvalue=None):
    it = iter(t)
    return izip_longest(*[it]*size, fillvalue=fillvalue)
Jochen Ritzel
fonte
A primeira função (par a par) parece não ter a clonagem e o avanço do segundo iterador. Veja a itertoolsseção de receitas.
Apalala
@Apalala: zip avança o mesmo iterador duas vezes.
Jochen Ritzel
Claro, você está correto, e em pares é o mais eficiente até agora, não sei por quê.
Apalala
1
Eu adoro essa solução: é preguiçosa e explora o estado dos iteradores com grande efeito. Você poderia até mesmo torná-lo uma linha, embora talvez à custa da legibilidade:izip(*[iter(t)]*size)
Channing Moore
para sua segunda solução, você não gostaria de evitar a criação de uma lista se for atrás do desempenho?
máx.
40

Eu diria que sua solução inicial pairs = zip(t[::2], t[1::2])é a melhor porque é mais fácil de ler (e em Python 3,zip retorna automaticamente um iterador em vez de uma lista).

Para garantir que todos os elementos sejam incluídos, você pode simplesmente estender a lista None .

Então, se a lista tiver um número ímpar de elementos, o ú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
fonte
6

Eu começo com um pequeno aviso - não use o código abaixo. Não é Pythônico de forma alguma, eu escrevi apenas para me divertir. É semelhante à pairwisefunção @ THC4k , mas usa itere fecha lambda. Não usa itertoolsmódulo e não suporta fillvalue. Eu coloquei aqui porque alguém pode achar interessante:

pairwise = lambda t: iter((lambda f: lambda: (f(), f()))(iter(t).next), None)
Tomasz Elendt
fonte
3

No que diz respeito à maioria dos pythônicos, eu diria que as receitas fornecidas nos documentos fonte do python (algumas das quais se parecem muito com as respostas que @JochenRitzel forneceu) são provavelmente sua melhor aposta;)

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)
Pat
fonte
2

Existe outra maneira "melhor" de percorrer uma lista em pares?

Não posso dizer com certeza, mas duvido: qualquer outro percurso incluiria mais código Python que precisa ser interpretado. As funções embutidas como zip () são escritas em C, que é muito mais rápido.

Qual seria a maneira certa de garantir que todos os elementos sejam incluídos?

Verifique o comprimento da lista e se for ímpar ( len(list) & 1 == 1), copie a lista e anexe um item.

Aaron Digulla
fonte
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
fonte
IndexError: pop from empty list
HQuser
@HQuser Claro, você obterá esse erro se tiver um número ímpar de itens na lista. Você deve ter certeza de que possui pares ou verificar esta condição de erro.
WaterMolecule de
0

Apenas faça isso:

>>> 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
fonte
Seu código é equivalente ao mais simples list(zip(l, l[1:]))e não divide a lista em pares.
Apalala
0

Aqui está um exemplo de criação de pares / pernas usando um gerador. Geradores estão livres de limites de pilha

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

Exemplo:

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

Resultado:

[(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]
Vlad Bezden
fonte
Comparação de tempo de execução?
Alan
A lista não é dividida em pares, já que a maioria dos números na lista original aparece em duas tuplas. A saída esperada é[(0, 1), (2, 3), (4, 5)....
Apalala
@Apalala, obrigado por apontar.
Corrigi
zip()já retorna um gerador em Python 3.x, @VladBezden
Apalala
-1

Caso alguém precise do algoritmo de resposta, aqui 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]]

Mas observe que sua lista original também será reduzida ao último elemento, porque você usou popnela.

>>> k
[4]
Frainmaster
fonte