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.


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)

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

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)

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)

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.


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

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

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

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]
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.