Função Transpor/Descompactar (inverso do zip)?
Pergunta
Eu tenho uma lista de tuplas de 2 itens e gostaria de convertê-las em 2 listas, onde a primeira contém o primeiro item de 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?
Solução
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 é ligando zip
com os argumentos:
zip(('a', 1), ('b', 2), ('c', 3), ('d', 4))
… exceto que os argumentos são passados para zip
diretamente (após ser convertido em uma tupla), portanto não há necessidade de se preocupar com o número de argumentos ficando muito grande.
Outras dicas
Você também poderia fazer
result = ([ a for a,b in original ], [ b for a,b in original ])
Isto deve escalar melhor.Especialmente se o Python conseguir não expandir a compreensão da lista, a menos que seja necessário.
(Aliás, ele forma duas tuplas (par) de listas, em vez de uma lista de tuplas, como zip
faz.)
Se os geradores em vez das 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 examinam a lista até que você solicite cada elemento, mas, por outro lado, mantêm referências à lista original.
Se você tiver listas que não tenham o mesmo comprimento, talvez não queira usar zip conforme a resposta de Patrick.Isso funciona:
>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
Mas com listas de comprimentos 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 map sem função para preencher resultados vazios com None:
>>> 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.
eu gosto de usar zip(*iterable)
(que é o trecho de código que você está procurando) em meus programas da seguinte forma:
def unzip(iterable):
return zip(*iterable)
eu acho unzip
mais legível.
>>> 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])
Fornece uma tupla de listas como na pergunta.
list1, list2 = [list(tup) for tup in zip(*original)]
Descompacta as duas listas.
É apenas outra maneira de fazer isso, mas me ajudou muito, então 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, então se você precisar de uma lista você pode usar:
x,y=(list(x),list(y))
Abordagem ingênua
def transpose_finite_iterable(iterable):
return zip(*iterable) # `itertools.izip` for Python 2 users
funciona bem para iterável finito (por exemplosequê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_ij
corresponde aj
-º elemento dei
-th iterável,
e depois de aplicar transpose_finite_iterable
Nós temos
| |a_00| |a_01| ... |a_0i| ... |
| |a_10| |a_11| ... |a_1i| ... |
| |... | |... | ... |... | ... |
| |a_n0| |a_n1| ... |a_ni| ... |
Exemplo Python de tal caso onde 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_iterable
novamente para retornar à estrutura original iterable
porque result
é um iterável infinito de iteráveis finitos (tuple
é 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 aí vem o deque
Depois de darmos uma olhada nos documentos de itertools.tee
função, existe 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 uma função geral para trabalhar com iteráveis de iteráveis, alguns dos quais são finitos e outros são potencialmente infinitos usando functools.singledispatch
decorador 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 "involuções") na classe de operadores binários sobre iteráveis finitos e não vazios.
Como bônus de singledispatch
podemos lidar numpy
matrizes como
import numpy as np
...
transpose.register(np.ndarray, np.transpose)
e então use-o como
>>> array = np.arange(4).reshape((2,2))
>>> array
array([[0, 1],
[2, 3]])
>>> transpose(array)
array([[0, 2],
[1, 3]])
Observação
Desde transpose
retorna iteradores e se alguém quiser ter um tuple
de list
é como no OP - isso pode ser feito adicionalmente com map
função integrada como
>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> tuple(map(list, transpose(original)))
(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
Anúncio
Eu adicionei solução generalizada para lz
pacote de 0.5.0
versã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)]
P.S.
Não há solução (pelo menos óbvia) para lidar com iteráveis potencialmente infinitos de iteráveis potencialmente infinitos, mas este caso é menos comum.
Como retorna tuplas (e pode usar muita 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
original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
#unzip
a1 , a2 = zip(*original)
#make tuple with two list
result=(list(a1),list(a2))
result
resultado=(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
Considere usar mais_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]]
Nenhuma das respostas anteriores eficientemente fornecer a saída necessária, que é um tupla de listas, ao invés de um lista de tuplas.Para o primeiro, você pode usar tuple
com 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 Python 2.7, onde zip
retorna uma lista em vez de um iterador.
Para Python 3.x, você precisará passar o resultado para uma função como list
ou tuple
para esgotar o iterador.Para iteradores com uso eficiente de memória, você pode omitir o externo list
e tuple
exige as respectivas soluções.
Enquanto zip(*seq)
é muito útil, pode ser inadequado para sequências muito longas, pois criará uma tupla de valores a serem passados.Por exemplo, 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 assim:
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ê quer fazer com o resultado, a escolha da coleção pode fazer muita diferença.No meu caso de uso real, usar conjuntos e nenhum loop interno é visivelmente mais rápido do que todas as outras abordagens.
E, como outros observaram, se você estiver fazendo isso com conjuntos de dados, pode fazer sentido usar coleções Numpy ou Pandas.
É assim que você pode transpor uma tupla 2x4 para uma tupla 4x2.
>>> tuple(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))
resultado
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]