Função de transposição / descompactação (inversa do zip)?

505

Eu tenho uma lista de tuplas de 2 itens e gostaria de convertê-las em 2 listas, onde o primeiro contém o primeiro item em cada tupla e a segunda lista contém o segundo item.

Por exemplo:

original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
# and I want to become...
result = (['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Existe uma função interna que faz isso?

Cristian
fonte
6
Ótimas respostas abaixo, mas também veja a transposição de numpy
#
3
Veja esta resposta agradável para fazer o mesmo com os geradores em vez de lista: how-to-unzip-um iterador-
YvesgereY

Respostas:

778

zipé o seu próprio inverso! Desde que você use o operador especial *.

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

A maneira como isso funciona é chamando zipcom os argumentos:

zip(('a', 1), ('b', 2), ('c', 3), ('d', 4))

… Exceto que os argumentos são passados zipdiretamente (após serem convertidos em uma tupla), então não há necessidade de se preocupar com o número de argumentos que estão ficando muito grandes.

Patrick
fonte
20
Ah, se fosse tão simples. Descompactar zip([], [])dessa maneira não o leva a você [], []. Isso te pega []. Se apenas ...
user2357112 suporta Monica
4
Isso não funciona no Python3. Veja: stackoverflow.com/questions/24590614/…
Tommy
31
@ Tommy Isso está incorreto. zipfunciona exatamente da mesma maneira no Python 3, exceto pelo fato de retornar um iterador em vez de uma lista. A fim de obter o mesmo resultado como acima você só precisa quebrar a chamada zip em uma lista: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))saída de vontade[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes
4
aviso: você pode encontrar problemas de memória e desempenho com listas muito longas.
Laurent LAPORTE
1
@ JohnP: lists estão bem. Mas, se você tentar obter o resultado completo de uma só vez (se listo resultado for zip), poderá usar muita memória (porque todos os tuples devem ser criados ao mesmo tempo). Se você puder apenas iterar o resultado zipsem listnegar, economizará muita memória. A única outra preocupação é se a entrada possui muitos elementos; o custo é que ele deve descompactá-los todos como argumentos e zipprecisará criar e armazenar iteradores para todos eles. Este é apenas um problema real com s muito longos list(pense em centenas de milhares de elementos ou mais).
ShadowRanger
29

Você também pode fazer

result = ([ a for a,b in original ], [ b for a,b in original ])

Ele deve dimensionar melhor. Especialmente se o Python fizer bom em não expandir as compreensões da lista, a menos que seja necessário.

(Aliás, ele cria duas tuplas (par) de listas, em vez de uma lista de tuplas, como zipfaz.)

Se geradores, em vez de listas reais, estiverem ok, isso faria o seguinte:

result = (( a for a,b in original ), ( b for a,b in original ))

Os geradores não vasculham a lista até você solicitar cada elemento, mas, por outro lado, eles mantêm referências à lista original.

Anders Eurenius
fonte
8
"Especialmente se o Python fizer bom em não expandir a compreensão da lista, a menos que seja necessário." mmm ... normalmente, as compreensões da lista são expandidas imediatamente - ou entendi algo errado?
glglgl
1
@glglgl: Não, você provavelmente está certo. Eu só esperava que alguma versão futura pudesse começar a fazer a coisa certa. (Não é impossível mudar, a semântica de efeitos colaterais que as mudanças precisam provavelmente já estão desanimados.)
Anders Eurenius
9
O que você espera obter é uma expressão de gerador - que já existe.
glglgl
12
Isso não é 'dimensionado melhor' que a zip(*x)versão. zip(*x)requer apenas uma passagem pelo loop e não utiliza elementos de pilha.
habnabit
1
Se "dimensiona melhor" ou não, depende do ciclo de vida dos dados originais em comparação com os dados transpostos. Essa resposta é melhor do que usar zipse o caso de uso é que os dados transpostos são usados ​​e descartados imediatamente, enquanto as listas originais permanecem na memória por muito mais tempo.
Ekevoo
21

Se você possui listas que não têm o mesmo tamanho, pode não querer usar o zip conforme a resposta do Patricks. Isso funciona:

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

Mas com listas de tamanhos diferentes, o zip trunca cada item para o comprimento da lista mais curta:

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', )])
[('a', 'b', 'c', 'd', 'e')]

Você pode usar o mapa sem função para preencher resultados vazios com Nenhum:

>>> map(None, *[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', )])
[('a', 'b', 'c', 'd', 'e'), (1, 2, 3, 4, None)]

zip () é um pouco mais rápido.

Chris
fonte
4
Você também pode usarizip_longest
Marcin
3
Conhecido como zip_longestpara usuários python3.
zezollo
1
@GrijeshChauhan Eu sei que isso é realmente antigo, mas é um recurso interno estranho: docs.python.org/2/library/functions.html#map "Se a função for Nenhuma, a função de identidade será assumida; se houver vários argumentos, map () retorna uma lista que consiste em tuplas contendo os itens correspondentes de todos os iteráveis ​​(um tipo de operação de transposição). Os argumentos iteráveis ​​podem ser uma sequência ou qualquer objeto iterável; o resultado é sempre uma lista. "
cactus1
18

Eu gosto de usar zip(*iterable)(que é o pedaço de código que você está procurando) nos meus programas da seguinte forma:

def unzip(iterable):
    return zip(*iterable)

Acho unzipmais legível.

wassimans
fonte
12
>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> tuple([list(tup) for tup in zip(*original)])
(['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Dá uma tupla de listas como na pergunta.

list1, list2 = [list(tup) for tup in zip(*original)]

Descompacta as duas listas.

Noyer282
fonte
8

Abordagem ingênua

def transpose_finite_iterable(iterable):
    return zip(*iterable)  # `itertools.izip` for Python 2 users

funciona bem para iterável finito (por exemplo, seqüências como list/ tuple/ str) de iteráveis ​​(potencialmente infinitos) que podem ser ilustrados como

| |a_00| |a_10| ... |a_n0| |
| |a_01| |a_11| ... |a_n1| |
| |... | |... | ... |... | |
| |a_0i| |a_1i| ... |a_ni| |
| |... | |... | ... |... | |

Onde

  • n in ℕ,
  • a_ijcorresponde ao j-th elemento de i-th iterável,

e depois de aplicar transpose_finite_iterablechegamos

| |a_00| |a_01| ... |a_0i| ... |
| |a_10| |a_11| ... |a_1i| ... |
| |... | |... | ... |... | ... |
| |a_n0| |a_n1| ... |a_ni| ... |

Exemplo em Python desse caso em que a_ij == j,n == 2

>>> from itertools import count
>>> iterable = [count(), count()]
>>> result = transpose_finite_iterable(iterable)
>>> next(result)
(0, 0)
>>> next(result)
(1, 1)

Mas não podemos usar transpose_finite_iterablenovamente para retornar à estrutura do original, iterableporque resulté um iterável infinito de iteráveis ​​finitos ( tuples no nosso caso):

>>> transpose_finite_iterable(result)
... hangs ...
Traceback (most recent call last):
  File "...", line 1, in ...
  File "...", line 2, in transpose_finite_iterable
MemoryError

Então, como podemos lidar com este caso?

... e aqui vem o deque

Depois de examinarmos os documentos de itertools.teefunção , há uma receita Python que, com algumas modificações, pode ajudar no nosso caso

def transpose_finite_iterables(iterable):
    iterator = iter(iterable)
    try:
        first_elements = next(iterator)
    except StopIteration:
        return ()
    queues = [deque([element])
              for element in first_elements]

    def coordinate(queue):
        while True:
            if not queue:
                try:
                    elements = next(iterator)
                except StopIteration:
                    return
                for sub_queue, element in zip(queues, elements):
                    sub_queue.append(element)
            yield queue.popleft()

    return tuple(map(coordinate, queues))

vamos checar

>>> from itertools import count
>>> iterable = [count(), count()]
>>> result = transpose_finite_iterables(transpose_finite_iterable(iterable))
>>> result
(<generator object transpose_finite_iterables.<locals>.coordinate at ...>, <generator object transpose_finite_iterables.<locals>.coordinate at ...>)
>>> next(result[0])
0
>>> next(result[0])
1

Síntese

Agora podemos definir a função geral para trabalhar com iteráveis ​​de iteráveis, das quais são finitas e outras são potencialmente infinitas usando functools.singledispatchdecorador como

from collections import (abc,
                         deque)
from functools import singledispatch


@singledispatch
def transpose(object_):
    """
    Transposes given object.
    """
    raise TypeError('Unsupported object type: {type}.'
                    .format(type=type))


@transpose.register(abc.Iterable)
def transpose_finite_iterables(object_):
    """
    Transposes given iterable of finite iterables.
    """
    iterator = iter(object_)
    try:
        first_elements = next(iterator)
    except StopIteration:
        return ()
    queues = [deque([element])
              for element in first_elements]

    def coordinate(queue):
        while True:
            if not queue:
                try:
                    elements = next(iterator)
                except StopIteration:
                    return
                for sub_queue, element in zip(queues, elements):
                    sub_queue.append(element)
            yield queue.popleft()

    return tuple(map(coordinate, queues))


def transpose_finite_iterable(object_):
    """
    Transposes given finite iterable of iterables.
    """
    yield from zip(*object_)

try:
    transpose.register(abc.Collection, transpose_finite_iterable)
except AttributeError:
    # Python3.5-
    transpose.register(abc.Mapping, transpose_finite_iterable)
    transpose.register(abc.Sequence, transpose_finite_iterable)
    transpose.register(abc.Set, transpose_finite_iterable)

que pode ser considerado como seu próprio inverso (os matemáticos chamam esse tipo de função de "involuções" ) na classe de operadores binários sobre iteráveis ​​finitos e não vazios.


Como bônus de singledispatching, podemos lidar com numpymatrizes como

import numpy as np
...
transpose.register(np.ndarray, np.transpose)

e depois usá-lo como

>>> array = np.arange(4).reshape((2,2))
>>> array
array([[0, 1],
       [2, 3]])
>>> transpose(array)
array([[0, 2],
       [1, 3]])

Nota

Desde que transposeretorna iteradores e se alguém quiser ter um tuplede lists como no OP - isso pode ser feito adicionalmente com mapa função incorporada como

>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> tuple(map(list, transpose(original)))
(['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Propaganda

Eu adicionei uma solução generalizada ao lzpacote da 0.5.0versão que pode ser usada como

>>> from lz.transposition import transpose
>>> list(map(tuple, transpose(zip(range(10), range(10, 20)))))
[(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), (10, 11, 12, 13, 14, 15, 16, 17, 18, 19)]

PS

Não há solução (pelo menos óbvia) para lidar com iterável potencialmente infinito de iteráveis ​​potencialmente infinitos, mas esse caso é menos comum.

Azat Ibrakov
fonte
4

É apenas outra maneira de fazer isso, mas me ajudou muito, então eu escrevo aqui:

Tendo esta estrutura de dados:

X=[1,2,3,4]
Y=['a','b','c','d']
XY=zip(X,Y)

Resultando em:

In: XY
Out: [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]

A maneira mais pitônica de descompactá-lo e voltar ao original é esta na minha opinião:

x,y=zip(*XY)

Mas isso retorna uma tupla, portanto, se você precisar de uma lista, poderá usar:

x,y=(list(x),list(y))
GM
fonte
3

Considere usar more_itertools.unzip :

>>> from more_itertools import unzip
>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> [list(x) for x in unzip(original)]
[['a', 'b', 'c', 'd'], [1, 2, 3, 4]]     
Neil G
fonte
1

Como retorna tuplas (e pode usar toneladas de memória), o zip(*zipped)truque parece mais inteligente do que útil para mim.

Aqui está uma função que realmente fornecerá o inverso do zip.

def unzip(zipped):
    """Inverse of built-in zip function.
    Args:
        zipped: a list of tuples

    Returns:
        a tuple of lists

    Example:
        a = [1, 2, 3]
        b = [4, 5, 6]
        zipped = list(zip(a, b))

        assert zipped == [(1, 4), (2, 5), (3, 6)]

        unzipped = unzip(zipped)

        assert unzipped == ([1, 2, 3], [4, 5, 6])

    """

    unzipped = ()
    if len(zipped) == 0:
        return unzipped

    dim = len(zipped[0])

    for i in range(dim):
        unzipped = unzipped + ([tup[i] for tup in zipped], )

    return unzipped
Waylon Flinn
fonte
Recriar tuplas continuamente não me parece tão eficiente, mas você pode estender essa abordagem usando deques que podem pré-alocar memória.
Charlie Clark
0

Nenhuma das respostas anteriores fornece com eficiência a saída necessária, que é uma tupla de listas , em vez de uma lista de tuplas . Para o primeiro, você pode usar tuplecom map. Aqui está a diferença:

res1 = list(zip(*original))              # [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
res2 = tuple(map(list, zip(*original)))  # (['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Além disso, a maioria das soluções anteriores assume o Python 2.7, onde zipretorna uma lista em vez de um iterador.

Para o Python 3.x, você precisará passar o resultado para uma função como listou tuplepara esgotar o iterador. Para iteradores com eficiência de memória, você pode omitir o externo liste tuplesolicitar as respectivas soluções.

jpp
fonte
0

Embora zip(*seq)seja muito útil, pode não ser adequado para seqüências muito longas, pois criará uma tupla de valores a serem passados. Por exemplo, eu tenho trabalhado com um sistema de coordenadas com mais de um milhão de entradas e acho significativamente mais rápido criar as sequências diretamente.

Uma abordagem genérica seria algo como isto:

from collections import deque
seq = ((a1, b1, …), (a2, b2, …), …)
width = len(seq[0])
output = [deque(len(seq))] * width # preallocate memory
for element in seq:
    for s, item in zip(output, element):
        s.append(item)

Mas, dependendo do que você deseja fazer com o resultado, a escolha da coleção pode fazer uma grande diferença. No meu caso de uso real, o uso de conjuntos e nenhum loop interno é notavelmente mais rápido que todas as outras abordagens.

E, como outros observaram, se você estiver fazendo isso com conjuntos de dados, pode fazer sentido usar as coleções Numpy ou Pandas.

Charlie Clark
fonte
0

Embora matrizes e pandas numpy possam ser preferíveis, essa função imita o comportamento de zip(*args)quando chamado como unzip(args).

Permite que os geradores sejam transmitidos conforme argsitera através dos valores. Decore clse / ou main_clsmicrogerencie a inicialização do contêiner.

def unzip(items, cls=list, main_cls=tuple):
    """Zip function in reverse.

    :param items: Zipped-like iterable.
    :type  items: iterable

    :param cls: Callable that returns iterable with callable append attribute.
        Defaults to `list`.
    :type  cls: callable, optional

    :param main_cls: Callable that returns iterable with callable append
        attribute. Defaults to `tuple`.
    :type  main_cls: callable, optional

    :returns: Unzipped items in instances returned from `cls`, in an instance
        returned from `main_cls`.

    :Example:

        assert unzip(zip(["a","b","c"],[1,2,3])) == (["a","b",c"],[1,2,3])
        assert unzip([("a",1),("b",2),("c",3)]) == (["a","b","c"],[1,2,3])
        assert unzip([("a",1)], deque, list) == [deque(["a"]),deque([1])]
        assert unzip((["a"],["b"]), lambda i: deque(i,1)) == (deque(["b"]),)
    """
    items = iter(items)

    try:
        i = next(items)
    except StopIteration:
        return main_cls()

    unzipped = main_cls(cls([v]) for v in i)

    for i in items:
        for c,v in zip(unzipped,i):
            c.append(v)

    return unzipped
Rastrear
fonte